@cryptotaxi247 / netdata-1 / commits / 18991fd4e

go.d/vsphere: migrate to framework v2 and expand coverage (#22458)

Co-authored-by: ilyam8 <ilya@netdata.cloud>

Costa Tsaousis committed May 23, 2026 at 12:47 UTC 18991fd4e128f441c9654186c7e0eb67cab20c7b
67 files changed +14557 -3571
.agents/skills/codacy-audit/SKILL.md
+20
@@ -72,6 +72,26 @@ $ .agents/skills/codacy-audit/scripts/analyze-local.sh
72
73 Run this before `git push`. If it returns 0 findings, the Codacy gate on the PR will be green (modulo Codacy server-side patterns the local CLI doesn't bundle). If it returns findings, fix them locally first.
74
75 +Operational gotcha: when the Dockerized Codacy CLI fails before a tool can emit
76 +results, the output file may have a `.json` suffix but contain tool-runner logs
77 +instead of JSON. Always verify with `jq empty <dump>` before treating a local
78 +dump as finding evidence. If GitHub check-run annotations are empty too, use
79 +`pr-issues.sh` with `CODACY_TOKEN`; without that token, record the evidence gap
80 +and re-check after the next push.
81 +
82 +Operational gotcha: the public Codacy v3 analysis endpoint can expose PR issue
83 +details even when GitHub check-run annotations are empty and no `CODACY_TOKEN`
84 +is available:
85 +
86 +```
87 +curl -fsS \
88 + "https://api.codacy.com/api/v3/analysis/organizations/gh/netdata/repositories/netdata/pull-requests/<PR>/issues?limit=100"
89 +```
90 +
91 +Filter for `.data[] | select(.deltaType == "Added")` to identify the issues
92 +that still block the PR. Treat `commitInfo` fields as sensitive operational
93 +metadata; do not copy names or email addresses into committed artifacts.
94 +
95 To restrict to a single tool (matches what Codacy reported on a CI run):
96
97 ```
.agents/skills/codacy-audit/how-tos/INDEX.md
+2
@@ -7,4 +7,6 @@ Skipping this rule means the next assistant repeats the analysis from scratch --
7 ## Entries
8
9 - [fetch-large-pr-issue-list](fetch-large-pr-issue-list.md) -- fix or verify the `jq: Argument list too long` failure mode by passing large Codacy issue arrays through a temporary file and `jq --slurpfile`.
10 +- [handle-malformed-local-json](handle-malformed-local-json.md) -- handle `analyze-local.sh` dumps that have a `.json` suffix but contain Codacy tool-runner logs instead of parseable JSON.
11 - [reproduce-pr-22423-markdownlint](reproduce-pr-22423-markdownlint.md) -- reproduce the 864 markdownlint findings PR #22423 saw on its first CI run, locally via `analyze-local.sh --tool markdownlint`.
12 +- [triage-action-required-without-token](triage-action-required-without-token.md) -- fetch public Codacy v3 PR issue details when GitHub check-run annotations are empty and no `CODACY_TOKEN` is available.
.agents/skills/codacy-audit/how-tos/handle-malformed-local-json.md new
+35
@@ -0,0 +1,35 @@
1 +# Handle Malformed Local Codacy JSON
2 +
3 +Use this when `analyze-local.sh` writes a `local-*.json` file but `jq` cannot
4 +parse it.
5 +
6 +1. Verify the dump before reading it as findings:
7 +
8 + ```bash
9 + jq empty .local/audits/codacy/local-*.json
10 + ```
11 +
12 +2. If parsing fails, inspect the first lines:
13 +
14 + ```bash
15 + sed -n '1,80p' .local/audits/codacy/local-*.json
16 + ```
17 +
18 +3. Treat tool-runner logs as a local-analysis failure, not as Codacy findings.
19 + A known failure mode is a Dockerized tool trying to read `/.codacyrc` as a
20 + file and reporting `read /.codacyrc: is a directory`.
21 +
22 +4. Check GitHub check-run annotations:
23 +
24 + ```bash
25 + gh api repos/netdata/netdata/check-runs/<CHECK_RUN_ID>/annotations --paginate
26 + ```
27 +
28 +5. If annotations are empty, fetch PR issues through the Codacy API:
29 +
30 + ```bash
31 + .agents/skills/codacy-audit/scripts/pr-issues.sh <PR_NUMBER>
32 + ```
33 +
34 +6. If `CODACY_TOKEN` is not configured, record that Codacy details are not
35 + locally available and re-check after the next push.
.agents/skills/codacy-audit/how-tos/triage-action-required-without-token.md new
+44
@@ -0,0 +1,44 @@
1 +# Triage `action_required` Without A Codacy Token
2 +
3 +Use this when the GitHub check-run says Codacy is `action_required`, but
4 +GitHub exposes no annotations and the local checkout has no `.env` with
5 +`CODACY_TOKEN`.
6 +
7 +1. Get the Codacy check-run summary from GitHub:
8 +
9 + ```bash
10 + gh api repos/netdata/netdata/check-runs/<check-run-id> \
11 + --jq '{conclusion:.conclusion, output:.output, details_url:.details_url}'
12 + ```
13 +
14 +2. Fetch public PR issue details from Codacy v3:
15 +
16 + ```bash
17 + curl -fsS \
18 + "https://api.codacy.com/api/v3/analysis/organizations/gh/netdata/repositories/netdata/pull-requests/<PR>/issues?limit=100" \
19 + -o .local/audits/codacy/pr-<PR>-public-issues.json
20 + ```
21 +
22 +3. Show only still-blocking issues:
23 +
24 + ```bash
25 + jq -r '
26 + .data[]
27 + | select(.deltaType == "Added")
28 + | [
29 + .commitIssue.filePath,
30 + .commitIssue.lineNumber,
31 + .commitIssue.patternInfo.id,
32 + .commitIssue.message,
33 + .commitIssue.lineText
34 + ]
35 + | @tsv
36 + ' .local/audits/codacy/pr-<PR>-public-issues.json
37 + ```
38 +
39 +4. Ignore `deltaType == "Fixed"` entries for the current blocker. They are
40 + historical issue records that Codacy already considers resolved.
41 +
42 +Safety note: the public response can contain `commitInfo` fields with personal
43 +metadata. Keep raw dumps under `.local/audits/codacy/`, which is gitignored,
44 +and do not copy names or email addresses into committed artifacts.
.agents/skills/integrations-lifecycle/gotchas.md
+15
@@ -64,6 +64,21 @@ before assuming the code does the obvious thing.
64
65 ## Dead / broken code in the pipeline
66
67 +### Metadata links may be `blob/master` or `edit/master`
68 +
69 +- File path: `integrations/gen_docs_integrations.py`.
70 +- `gen_integrations.py` can emit metadata links in GitHub
71 + `blob/master` form, while older docs-generation code only
72 + stripped `edit/master`.
73 +- If `build_path()` does not normalize both forms, scoped
74 + generation such as
75 + `python3 integrations/gen_docs_integrations.py -c go.d.plugin/vsphere`
76 + finds the collector in `integrations.js` but writes nothing
77 + because the derived local path does not exist.
78 +- Current contract: `build_path()` must strip both
79 + `blob/master/` and `edit/master/` before removing
80 + `/metadata.yaml`.
81 +
82 ### `integrations/check_collector_metadata.py` is broken
83
84 - File path: `integrations/check_collector_metadata.py`.
.agents/skills/project-writing-go-modules-framework-v2/SKILL.md new
+101
@@ -0,0 +1,101 @@
1 +---
2 +name: project-writing-go-modules-framework-v2
3 +description: Use when creating or migrating a Go go.d collector to framework V2, touching CollectorV2, metrix.CollectorStore, ChartTemplateYAML/charts.yaml, charttpl/chartengine, V2 host scopes/vnodes, or V2 collector tests. Focuses on concise maintainer-preferred V2 collector patterns.
4 +---
5 +
6 +# Writing Go go.d Modules With Framework V2
7 +
8 +Use with `project-writing-collectors`. Keep this skill loaded for style; read
9 +source files for evidence.
10 +
11 +## Read First
12 +
13 +- Contract: `src/go/plugin/framework/collectorapi/collector.go`
14 +- Runtime/chart lifecycle: `src/go/plugin/framework/chartengine/README.md`
15 +- Template format: `src/go/plugin/framework/charttpl/README.md`
16 +- Host scopes/vnodes: `.agents/sow/specs/go-v2-host-scope.md`
17 +- Closest examples:
18 + - `src/go/plugin/go.d/collector/azure_monitor/` for dynamic scopes/profiles.
19 + - `src/go/plugin/go.d/collector/ping/` for the smallest V2 shape.
20 + - `src/go/plugin/go.d/collector/mysql/` for migration compatibility.
21 + - `src/go/plugin/go.d/collector/powervault/` and `powerstore/` for remote
22 + discovery, labels, and chart templates.
23 +
24 +## Core Style
25 +
26 +- Register with `CreateV2`; expose `Config: func() any { return &Config{} }`.
27 +- `New()` owns defaults, `metrix.NewCollectorStore()`, and test seams.
28 +- Store `metrix.CollectorStore`; implement `MetricStore()`.
29 +- Implement `ChartTemplateYAML()`; prefer embedded `charts.yaml`.
30 +- `Collect(ctx)` returns `error` and writes metrics to `metrix`; it does not
31 + return a V1 `map[string]int64`.
32 +- Keep files boring: `collector.go`, `collect.go`, `metrics.go`,
33 + `charts.yaml`, focused domain helpers, focused tests.
34 +
35 +## Metrics And Charts
36 +
37 +- Create instruments once when the metric surface is known.
38 +- Use `store.Write().SnapshotMeter("")` for normal metrics.
39 +- Use `Vec(...)` for labels, `Gauge` for current values,
40 + `Counter.ObserveTotal()` for source counters, and `StateSet` for fixed
41 + one-active-state values.
42 +- Use stable metric names that `charts.yaml` selects.
43 +- In `charts.yaml`: use `version: v1`, `context_namespace`, `instances.by_labels`,
44 + `algorithm: incremental` for counters, and `absolute` for gauges.
45 +- Put multipliers, divisors, hidden flags, and float formatting in the chart
46 + template, not ad hoc chart-emission code.
47 +
48 +## Compatibility Rules
49 +
50 +- For migrations, first create a compatibility manifest covering chart IDs,
51 + contexts, dimension IDs/names, labels, config keys, DynCfg schema keys,
52 + stock config, alerts, docs, and lifecycle behavior.
53 +- Preserve existing public contracts unless the SOW records an explicit breaking
54 + decision.
55 +- Keep old YAML/JSON field names. Add new config as opt-in when cardinality,
56 + cost, or user-visible identity could surprise existing users.
57 +- Keep `metadata.yaml`, `config_schema.json`, stock config, health alerts, and
58 + README synchronized with code.
59 +- Never log raw secrets, DSNs, bearer tokens, or URLs with embedded credentials.
60 +
61 +## Hot-Path Logging
62 +
63 +- Do not emit `Warningf`/`Errorf` every collection cycle for a recoverable
64 + partial failure. Use the built-in logger limiter:
65 + `c.Limit("collector:stable-operation-key", 1, time.Hour).Warningf(...)`.
66 +- Keep limiter keys stable and low-cardinality. Use operation names, not entity
67 + IDs, labels, URLs, raw errors, or user-controlled values.
68 +- `Once()` is reset by `JobV2.runOnce()`, so it is useful inside one cycle only;
69 + it is not cross-cycle spam protection.
70 +- Full collection failure should still return an error with context so the job
71 + retry path handles it. Limit only fail-soft warnings/errors where collection
72 + continues with partial or stale data.
73 +
74 +## Host Scopes
75 +
76 +- Use host scopes only after a product decision says the data belongs on a
77 + generated vnode.
78 +- Keep `ScopeKey` and `GUID` deterministic.
79 +- Add `_vnode_type=<source>` on collector-generated vnodes.
80 +- Bound and document cardinality. Do not create VM/disk/NIC/path/sensor scopes
81 + by default.
82 +
83 +## Tests
84 +
85 +At minimum, V2 work needs:
86 +
87 +- config YAML/JSON serialization compatibility;
88 +- `Init`, `Check`, `Collect`, and `Cleanup` lifecycle coverage;
89 +- explicit metric-store cycle tests with `BeginCycle`, success commit, and abort
90 + on expected collection errors;
91 +- chart-template schema/decode/validate/compile coverage;
92 +- chart coverage assertions for fixtures expected to materialize all dimensions;
93 +- host-scope tests when scopes/vnodes are used.
94 +
95 +## Pre-PR Check
96 +
97 +- No V1 `map[string]int64` collection path remains unless intentionally kept for
98 + a compatibility bridge.
99 +- Existing public chart/metric/config identity is preserved.
100 +- New labels and scopes are bounded and documented.
101 +- Enrichment is split from the V2 compatibility migration when possible.
.agents/sow/done/SOW-0015-20260507-vsphere-v2-parity-enrichment.md new
+557
@@ -0,0 +1,557 @@
1 +# SOW-0015 - vSphere V2 Parity And Enrichment
2 +
3 +## Status
4 +
5 +Status: completed
6 +
7 +Sub-state: Implementation is complete for the approved PR scope. The SOW has
8 +been collapsed to the final shipped state by user decision. Final validation
9 +passed on 2026-05-23, and this SOW is being moved to `.agents/sow/done/` with
10 +the closeout commit.
11 +
12 +## Requirements
13 +
14 +### Purpose
15 +
16 +Migrate the Go vSphere collector to framework V2 while preserving the useful V1
17 +metric contract, adding approved vSphere parity and enrichment surfaces, and
18 +removing transitional or high-cardinality features that should not ship in this
19 +PR.
20 +
21 +### User Request
22 +
23 +The user requested a clean end state for the vSphere V2 migration and parity
24 +work, including:
25 +
26 +- framework V2 collection and chart templates;
27 +- compatibility with the existing vSphere metric surface where accepted;
28 +- default-safe additive object-level metrics;
29 +- opt-in datastore-cluster, vSAN, tag/custom-attribute, and topology surfaces;
30 +- removal of config/options/features that are not worth shipping now;
31 +- charts.yaml as the single chart source of truth;
32 +- collector taxonomy that passes the fatal taxonomy CI gate;
33 +- tests converted toward table-driven and V2 metric-store assertions;
34 +- a final SOW that records the end state rather than the development diary.
35 +
36 +### Acceptance Criteria
37 +
38 +- The vSphere collector registers and runs through framework V2.
39 +- Current default VM, host, datastore, cluster, and resource-pool metric
40 + contexts remain available with accepted labels, dimensions, units, and values.
41 +- Chart IDs may change to framework V2 instance chart IDs; `id` is the stable
42 + vSphere managed-object-reference label used for chart instances.
43 +- `charts.yaml` is authoritative. The old Go chart-template mirror, runtime
44 + chart bridge, and V1 golden fixture are removed.
45 +- Default-safe metrics for snapshots, VM/host/cluster state, datastore state,
46 + inventory counts, and aggregate power are implemented.
47 +- Optional datastore-cluster and vSAN metrics are default-off and selector
48 + controlled.
49 +- Optional vSphere tag/custom-attribute labels are default-off and allowlist
50 + controlled.
51 +- Optional network topology discovery is default-off and exposed only through
52 + the cached topology Function.
53 +- VM/host child-instance metric surfaces, generated ESXi/VM vnodes,
54 + inventory-path labels, VM guest labels, host/VM power-state config controls,
55 + and the power-metrics config knob are not part of the shipped public surface.
56 +- `metadata.yaml`, `config_schema.json`, stock `go.d/vsphere.conf`,
57 + `charts.yaml`, health alerts, generated integration docs, and
58 + `taxonomy.yaml` are consistent with the final code.
59 +- Local validation covers Go tests, vet, chart-template compilation, taxonomy
60 + checks, docs/schema parsing, and targeted reviewer feedback.
61 +
62 +## Final Scope
63 +
64 +### Framework And Runtime
65 +
66 +- Registration uses framework V2 via `CreateV2`.
67 +- `Collect(ctx)` writes directly to `metrix.CollectorStore`.
68 +- The legacy `map[string]int64` collection bridge is gone.
69 +- `charts.yaml` is embedded and returned by `ChartTemplateYAML()`.
70 +- Runtime chart mutation is gone; chart lifecycle is handled by chartengine and
71 + chart-template `expire_after_cycles`.
72 +- Metric writers emit final V2 metric names directly and consistently attach
73 + the `id` label plus resource-specific labels.
74 +
75 +### Configuration Contract
76 +
77 +Current vSphere-specific config keys:
78 +
79 +- target and scheduling: `url`, `username`, `password`, `timeout`,
80 + `discovery_interval`, `update_every`, `autodetection_retry`, `vnode`;
81 +- core include selectors: `host_include`, `vm_include`,
82 + `datastore_include`, `cluster_include`;
83 +- optional labels: `tag_categories`, `custom_attributes`;
84 +- optional datastore clusters: `collect_datastore_clusters`,
85 + `datastore_cluster_include`;
86 +- optional vSAN: `collect_vsan`, `vsan_cluster_include`,
87 + `vsan_host_include`, `vsan_vm_include`;
88 +- optional topology: `collect_network_topology`;
89 +- inherited HTTP/TLS/proxy settings from `web.HTTPConfig`, with unused
90 + method/body-style fields hidden from the dynamic configuration UI.
91 +
92 +Removed before merge:
93 +
94 +- `max_*` resource caps;
95 +- `collect_power_metrics`;
96 +- `host_power_states`, `vm_power_states`;
97 +- `collect_inventory_path_label`, `vm_guest_labels`;
98 +- `esxi_vnodes`, `vm_vnodes`;
99 +- VM child-instance options:
100 + `collect_vm_disks`, `collect_vm_disk_performance`, `vm_disk_include`,
101 + `collect_vm_nic_performance`, `vm_nic_include`;
102 +- host child-instance options:
103 + `collect_host_nic_performance`, `host_nic_include`,
104 + `collect_host_disk_performance`, `host_disk_include`,
105 + `collect_host_storage_adapter_performance`,
106 + `host_storage_adapter_include`,
107 + `collect_host_storage_path_performance`, `host_storage_path_include`,
108 + `collect_host_cpu_instance_performance`,
109 + `host_cpu_instance_include`.
110 +
111 +### Metric Surface
112 +
113 +The final metric surface is documented in `collector/vsphere/metadata.yaml` and
114 +charted in `collector/vsphere/charts.yaml`.
115 +
116 +Default metric groups:
117 +
118 +- inventory object counts;
119 +- VM aggregate CPU, memory, swap, disk, network, power state, connection state,
120 + tools state, consolidation state, uptime, configuration, storage usage, and
121 + snapshot metrics;
122 +- host aggregate CPU, memory, swap, disk, network, overall status, power state,
123 + connection state, maintenance state, uptime, power, and energy metrics;
124 +- datastore aggregate I/O, IOPS, latency, space, accessibility, maintenance,
125 + multiple-host-access, and overall status metrics;
126 +- cluster capacity, topology, utilization, DRS, HA, vMotion, VM operation,
127 + inventory, and overall status metrics;
128 +- resource-pool CPU, memory, allocation, config, and status metrics.
129 +
130 +Optional metric groups:
131 +
132 +- datastore-cluster space, Storage DRS status, and overall status metrics behind
133 + `collect_datastore_clusters`;
134 +- vSAN cluster, host, and VM capacity/performance/health metrics behind
135 + `collect_vsan`.
136 +
137 +Aggregate VM and host disk/network metrics remain default-on. Per-disk,
138 +per-NIC, per-storage-adapter, per-storage-path, and per-CPU-instance metrics are
139 +excluded from this PR.
140 +
141 +### Labels
142 +
143 +Every emitted series includes the V2 `id` label.
144 +
145 +Resource-specific labels:
146 +
147 +- VM: `datacenter`, `cluster`, `host`, `vm`;
148 +- host: `datacenter`, `cluster`, `host`;
149 +- datastore: `datacenter`, `datastore`, `type`;
150 +- cluster: `datacenter`, `cluster`;
151 +- resource pool: `datacenter`, `cluster`, `resource_pool`;
152 +- datastore cluster: `datacenter`, `datastore_cluster`;
153 +- vSAN cluster: `datacenter`, `cluster`, `vsan_uuid`;
154 +- vSAN host: `datacenter`, `cluster`, `host`, `vsan_node_uuid`;
155 +- vSAN VM: `datacenter`, `cluster`, `host`, `vm`, `vm_instance_uuid`;
156 +- inventory: `id=inventory`.
157 +
158 +Optional enrichment labels:
159 +
160 +- `vsphere_tag_<category>` for tag categories matched by `tag_categories`;
161 +- `vsphere_custom_attribute_<name>` for custom attributes matched by
162 + `custom_attributes`.
163 +
164 +Tag/custom-attribute names are sanitized for label keys. Multiple tags in one
165 +category are sorted and joined with `|`. Users are warned not to allowlist
166 +categories or attributes that may contain secrets or sensitive data.
167 +
168 +Standalone-host dummy clusters are detected by `domain-s*` cluster IDs, not by
169 +name equality.
170 +
171 +### Functions And Topology
172 +
173 +- `vsphere:readiness` is a read-only cached Function. It reports local cached
174 + readiness, configured optional gates, and discovered resource counts without
175 + issuing extra vCenter API calls.
176 +- `topology:vsphere` is the public cached topology Function alias. It emits
177 + topology actors and links for datacenters, clusters, hosts, VMs, datastores,
178 + datastore clusters, and resource pools from cached discovery state.
179 +- `collect_network_topology` adds vSphere Network and Distributed Virtual Port
180 + Group actors and host/VM network links to topology output only. It does not
181 + create charts or metric labels.
182 +- `opaqueNetwork-` managed-object IDs map to `vsphere_network`, so NSX-backed
183 + network links do not break.
184 +
185 +### Dashboard And Documentation Artifacts
186 +
187 +- `charts.yaml` is the single source for chart templates.
188 +- `taxonomy.yaml` places vSphere under `containers-vms` and mirrors the
189 + existing cloud-frontend vSphere dashboard TOC:
190 + heads grid, Inventory, Clusters, Hosts, Virtual Machines, Resource Pools,
191 + Datastores, and Datastore Clusters.
192 +- `metadata.yaml`, `config_schema.json`, stock `go.d/vsphere.conf`, generated
193 + integration markdown, and `health.d/vsphere.conf` match the final config and
194 + metric surface.
195 +
196 +## Out Of Scope
197 +
198 +These items are intentionally not part of this PR:
199 +
200 +- vCenter/ESXi events and logs;
201 +- collector-generated ESXi and VM vnodes;
202 +- datastore vnodes;
203 +- inventory-path labels;
204 +- VM guest hostname, IP address, and guest OS labels;
205 +- MAC, IQN, WWN, datastore path, and other sensitive device identity labels;
206 +- VM and host child-instance metric families;
207 +- deeper vSAN internals such as disk-group, disk, component, CMMDS, and all
208 + Telegraf-style entity-type metrics;
209 +- live permission probes in readiness;
210 +- VCSA appliance health metrics, which belong to the `vcsa` collector;
211 +- ESXi hardware sensors, which are covered by SNMP `vmware-esx`;
212 +- workload/container metrics inside VMs, which belong to guest agents or
213 + Kubernetes collectors.
214 +
215 +Any of these requires a separate user-approved product/NIDL decision before
216 +implementation.
217 +
218 +## Analysis
219 +
220 +Sources checked:
221 +
222 +- `collector/vsphere/*.go` and subpackages;
223 +- `collector/vsphere/charts.yaml`;
224 +- `collector/vsphere/metadata.yaml`;
225 +- `collector/vsphere/config_schema.json`;
226 +- `collector/vsphere/taxonomy.yaml`;
227 +- `config/go.d/vsphere.conf`;
228 +- `health.d/vsphere.conf`;
229 +- `integrations/check_collector_taxonomy.py`;
230 +- `.agents/sow/specs/vsphere-parity-matrix.md`;
231 +- `.agents/sow/specs/vsphere-v1-compatibility-manifest.md`;
232 +- `.agents/sow/specs/go-v2-host-scope.md`;
233 +- project skills for collector authoring, framework V2 modules, and integration
234 + lifecycle.
235 +
236 +External/source evidence captured in
237 +`.agents/sow/specs/vsphere-parity-matrix.md` includes Broadcom vSphere/vSAN API
238 +documentation and checked open-source implementations from Datadog, Telegraf,
239 +Grafana vmware_exporter, Elastic Beats, Zabbix, New Relic, OpenTelemetry
240 +Collector Contrib, and Grafana Alloy.
241 +
242 +Root-cause model:
243 +
244 +- The pre-existing vSphere collector had a V1 runtime/chart model and a narrower
245 + metric surface.
246 +- Framework V2 requires a static chart-template contract and metric-store
247 + writers instead of runtime chart mutation.
248 +- Some parity candidates are useful object-level metrics; others are
249 + high-cardinality or sensitive identity surfaces that should not be exposed as
250 + broad public config in this PR.
251 +- The final implementation keeps default behavior useful and bounded, makes
252 + costly/sensitive additions opt-in, and removes transitional APIs before merge.
253 +
254 +Primary risks:
255 +
256 +- vCenter/vSAN live API behavior can differ from simulator behavior. Local tests
257 + cover typed govmomi APIs and parser behavior, but no real production vCenter
258 + was available in this worktree.
259 +- Chart ID continuity is intentionally broken by the V2 migration. Contexts,
260 + dimensions, labels, units, and values are preserved where accepted.
261 +- Optional vSAN APIs require privileges and vSAN availability not represented by
262 + the simulator.
263 +- Optional tag/custom-attribute enrichment can expose sensitive data if users
264 + allowlist sensitive categories or attributes; docs and config descriptions
265 + warn about this.
266 +
267 +Sensitive data handling plan:
268 +
269 +- Durable artifacts contain no real credentials, tokens, customer names,
270 + customer endpoints, private endpoints, or customer-identifying IP addresses.
271 +- Examples use placeholders or generic local names.
272 +- SOW evidence cites file paths, commands, and sanitized findings rather than
273 + raw vCenter data.
274 +
275 +## Pre-Implementation Gate
276 +
277 +Status: satisfied.
278 +
279 +Affected contracts and surfaces:
280 +
281 +- Go collector registration and runtime lifecycle;
282 +- vSphere discovery and scraping;
283 +- chart contexts, dimensions, labels, units, priorities, and lifecycle;
284 +- dynamic configuration schema;
285 +- stock configuration;
286 +- integration metadata and generated documentation;
287 +- health alerts;
288 +- collector taxonomy;
289 +- project specs and SOW lifecycle.
290 +
291 +Existing patterns reused:
292 +
293 +- framework V2 `CollectorStore`, `ChartTemplateYAML`, and chartengine tests;
294 +- `collecttest.AssertChartCoverage` and V2 metric-store assertions;
295 +- `web.HTTPConfig` embedding with UI-hidden unused fields;
296 +- optional allowlist and selector patterns for bounded/sensitive data;
297 +- cached read-only Functions for supportability/topology surfaces;
298 +- fail-soft collection for optional enrichment and partial API failures.
299 +
300 +Implementation plan:
301 +
302 +1. Migrate collector runtime and charts to framework V2.
303 +2. Preserve accepted V1 metric semantics and add default-safe object-level
304 + parity metrics.
305 +3. Add approved opt-in datastore-cluster, vSAN, label enrichment, and topology
306 + surfaces.
307 +4. Remove rejected or superseded public config and high-cardinality child
308 + metric surfaces.
309 +5. Make `charts.yaml` and `taxonomy.yaml` authoritative source artifacts.
310 +6. Rewrite tests around V2 metric-store output and chart-template coverage.
311 +7. Update docs, schema, stock config, health alerts, specs, and SOW.
312 +
313 +Validation plan:
314 +
315 +- focused unit tests for parsers, matchers, discovery, writers, Functions, and
316 + review feedback;
317 +- full vSphere package tests;
318 +- Go vet;
319 +- chart-template schema/decode/compile checks;
320 +- taxonomy gate and exact metadata-to-taxonomy ownership checks;
321 +- JSON/YAML parse checks;
322 +- generated integration docs when metadata/config changes;
323 +- same-failure grep for removed public keys and bridge symbols.
324 +
325 +Open decisions:
326 +
327 +- None for the implemented PR scope.
328 +
329 +## Final User Decisions
330 +
331 +1. Keep the vSphere work in one PR and split by focused commits.
332 +2. Use framework V2 and accept framework V2 chart ID changes.
333 +3. Preserve contexts, dimensions, labels, units, and meaning where accepted.
334 +4. Use `charts.yaml` as the only chart-template source.
335 +5. Use final V2 metric-store assertions instead of legacy runtime-chart maps.
336 +6. Keep datastore-cluster and vSAN metrics opt-in.
337 +7. Keep tag/custom-attribute labels opt-in with allowlists.
338 +8. Remove generated ESXi/VM vnodes from this PR.
339 +9. Remove inventory-path and VM guest labels from this PR.
340 +10. Remove VM and host child-instance metric surfaces from this PR.
341 +11. Remove host/VM power-state config controls.
342 +12. Remove `collect_power_metrics`; aggregate power metrics are part of the
343 + shipped metric surface when vSphere exposes the counters.
344 +13. Remove `max_*` caps from this collector before merge.
345 +14. Keep vCenter/ESXi events out of this metrics PR.
346 +15. Add `taxonomy.yaml` because the taxonomy gate is fatal.
347 +16. Base vSphere taxonomy shape on the existing cloud-frontend vSphere TOC.
348 +17. Collapse SOW-0015 to final-state evidence instead of development history.
349 +
350 +## Implementation Summary
351 +
352 +Runtime:
353 +
354 +- Migrated collector registration and collection to framework V2.
355 +- Added direct V2 gauge observation path.
356 +- Removed V1 runtime chart bridge, Go chart mirror, V1 compatibility golden
357 + fixture, and legacy metric-map assertions.
358 +- Kept deterministic sorting helpers for stable output and tests.
359 +
360 +Discovery and scraping:
361 +
362 +- Discovery includes selected non-powered hosts/VMs for property/status metrics
363 + while skipping real-time performance scraping where vSphere has no useful
364 + samples.
365 +- Datastore, cluster, and resource-pool property refresh failures skip stale
366 + property metrics and let chartengine lifecycle handle expiry.
367 +- Missing performance counters warn once per stable key and do not abort the
368 + whole collector.
369 +- vSphere client cleanup handles partial initialization and session logout.
370 +
371 +Metrics:
372 +
373 +- Added VM snapshot count, maximum age, and maximum chain depth.
374 +- Added VM/host/cluster/datastore/resource-pool property/status metrics.
375 +- Added inventory object counts.
376 +- Added aggregate host/VM power and energy metrics.
377 +- Added optional datastore-cluster metrics.
378 +- Added optional vSAN metrics.
379 +- Preserved aggregate VM/host datastore/network/disk/cluster/resource-pool
380 + metrics accepted from V1.
381 +
382 +Configuration and selectors:
383 +
384 +- Added typed include selector types for core path includes and optional
385 + datastore-cluster/vSAN selectors while preserving YAML/JSON keys.
386 +- Extracted reusable ordered simple-pattern list matching to `pkg/matcher`.
387 +- Preserved config-specific validation and error-message shapes.
388 +- Removed public keys that should not ship in this PR.
389 +
390 +Labels and enrichment:
391 +
392 +- Added optional vSphere tag and custom-attribute labels with allowlists.
393 +- Preserved empty gates so REST/CIS/tag/custom-attribute clients are not used
394 + when enrichment is disabled.
395 +- Added privacy warnings for user metadata labels.
396 +
397 +Functions and topology:
398 +
399 +- Added cached readiness Function.
400 +- Added cached topology Function.
401 +- Added optional network/DVPG topology discovery.
402 +- Added `opaqueNetwork-` topology ID support.
403 +
404 +Tests:
405 +
406 +- Reworked collector tests to V2 metric-store reads and table-driven cases where
407 + setup/assertions are shared.
408 +- Added chart-template schema/decode/priority/compile validation.
409 +- Added `AssertChartCoverage` and selector-match checks for default and
410 + optional surfaces.
411 +- Added focused tests for matchers, vSAN parsing, datastore clusters, power
412 + metrics, labels, topology, readiness, discovery, client cleanup, and reviewer
413 + feedback.
414 +- Replaced fixed task lifecycle sleeps with deterministic `task.wait()`.
415 +
416 +Artifacts:
417 +
418 +- Updated `charts.yaml`, `metadata.yaml`, `config_schema.json`, stock
419 + `go.d/vsphere.conf`, health alerts, generated integration markdown, and
420 + `taxonomy.yaml`.
421 +- Updated specs for vSphere parity and the superseded V1 compatibility manifest.
422 +- Added/updated project skill guidance for framework V2 collector work.
423 +
424 +## Validation
425 +
426 +Acceptance criteria evidence:
427 +
428 +- `collector/vsphere/charts.go`,
429 + `collector/vsphere/chart_template_sets.go`,
430 + `collector/vsphere/compat_manifest_test.go`, and
431 + `collector/vsphere/testdata/v1_compat_manifest.json` are removed.
432 +- Grep for runtime bridge symbols such as `chartTemplateSets`, `legacyDimID`,
433 + `v2MetricName`, `writeChartMetrics`, `chartExpireAfterCycles`, and old
434 + `*ChartsTmpl` names returns no production hits.
435 +- Grep for removed config keys under vSphere code/config/docs surfaces returns
436 + no non-SOW hits.
437 +- `metadata.yaml`, `charts.yaml`, and taxonomy coverage agree on the current
438 + vSphere contexts.
439 +
440 +Tests and checks run during final state:
441 +
442 +- `go test -count=1 -timeout 300s ./collector/vsphere/...` passed from
443 + `src/go/plugin/go.d`.
444 +- `go vet ./collector/vsphere/...` passed from `src/go/plugin/go.d`.
445 +- `go test -count=1 -run '^Test_task' ./collector/vsphere` passed from
446 + `src/go/plugin/go.d`.
447 +- `../../../../.venv/bin/python ../../../../integrations/check_collector_taxonomy.py --pr-diff upstream/master...HEAD`
448 + passed from `src/go/plugin/go.d`.
449 +- `../../../../.venv/bin/python ../../../../integrations/gen_taxonomy.py --check-only`
450 + passed from `src/go/plugin/go.d`.
451 +- Manual taxonomy ownership check reported
452 + `metadata=115 owned=115 referenced=4 missing=0 extra=0 duplicates=0`.
453 +- `python3 -m json.tool collector/vsphere/config_schema.json` passed.
454 +- YAML parse checks for `collector/vsphere/metadata.yaml`,
455 + `collector/vsphere/charts.yaml`, `collector/vsphere/taxonomy.yaml`, and stock
456 + `config/go.d/vsphere.conf` passed during the PR work.
457 +- Generated integration markdown was regenerated when metadata/config changed.
458 +- `git diff --check` passed for final touched files.
459 +
460 +Reviewer findings:
461 +
462 +- All accepted GitHub review feedback was fixed:
463 + readiness client-state check, opaque network topology IDs, discoverer pointer
464 + receiver warning dedup, datastore-cluster Storage DRS unknown state,
465 + dummy-cluster label detection, datastore writer exact metric count, and task
466 + lifecycle test synchronization.
467 +- Findings rejected as out of scope or not applicable are reflected in the final
468 + out-of-scope section and specs.
469 +
470 +Same-failure scans:
471 +
472 +- Removed public config keys were searched across vSphere code, config, schema,
473 + metadata, stock config, and tests.
474 +- Removed chart bridge symbols were searched across production Go code.
475 +- Taxonomy contexts were compared against metadata contexts with missing, extra,
476 + and duplicate checks.
477 +
478 +Sensitive data gate:
479 +
480 +- Durable artifacts contain placeholders only for credentials and endpoints.
481 +- No raw secrets, bearer tokens, private keys, session cookies, customer names,
482 + customer-identifying non-private IP addresses, private endpoints, or
483 + proprietary incident data were added.
484 +
485 +Artifact maintenance gate:
486 +
487 +- `AGENTS.md`: no final update required; existing collector consistency and SOW
488 + rules already cover this work.
489 +- Runtime project skills: framework V2 collector guidance exists under
490 + `.agents/skills/project-writing-go-modules-framework-v2/`; integration
491 + lifecycle guidance was followed for taxonomy/metadata/doc artifacts.
492 +- Specs: `.agents/sow/specs/vsphere-parity-matrix.md` records final parity
493 + classifications; `.agents/sow/specs/vsphere-v1-compatibility-manifest.md`
494 + records the superseded V1 baseline and current V2 validation replacements.
495 +- End-user/operator docs: `metadata.yaml`, stock config, generated integration
496 + markdown, and health alerts were updated with the shipped config/metric
497 + surface.
498 +- End-user/operator skills: no public operator skill changed because this PR
499 + changes collector behavior/docs, not AI skill workflows.
500 +- SOW lifecycle: this closeout marks SOW-0015 `completed` and moves it to
501 + `.agents/sow/done/` with the closing commit.
502 +
503 +Specs update:
504 +
505 +- vSphere parity matrix is the current WHAT contract for included, excluded,
506 + covered-elsewhere, and non-metric surfaces.
507 +- V1 compatibility manifest is explicitly superseded and retained only as
508 + historical baseline evidence.
509 +
510 +Project skills update:
511 +
512 +- Framework V2 collector skill captures reusable HOW-to-work guidance for this
513 + migration pattern.
514 +- No additional skill update is required by the final SOW rewrite.
515 +
516 +Documentation update:
517 +
518 +- vSphere integration docs are generated from `metadata.yaml`.
519 +- `README.md` follows the generated integration markdown symlink pattern.
520 +- `taxonomy.yaml` is now present and passes the fatal taxonomy gate.
521 +
522 +Lessons:
523 +
524 +- High-cardinality child-instance surfaces should not be added only because
525 + vendor APIs expose them. They need a clear user need and bounded product
526 + contract.
527 +- V2 migrations should move to direct metric-store assertions early; keeping a
528 + runtime V1 chart bridge makes tests and implementation harder to reason about.
529 +- Collector taxonomy is a required source artifact when metric contexts change.
530 +- Fixed sleeps in goroutine lifecycle tests should use explicit synchronization
531 + primitives when the implementation exposes them.
532 +
533 +## Outcome
534 +
535 +Implementation is complete for the approved PR scope. The collector now has a
536 +clean framework V2 runtime, authoritative YAML chart/taxonomy artifacts,
537 +approved parity/enrichment surfaces, and tests that assert the final V2 metric
538 +store and chart-template behavior.
539 +
540 +## Followup
541 +
542 +No follow-up SOW is required for the implemented scope.
543 +
544 +Excluded work that requires a separate user-approved SOW before implementation:
545 +
546 +- vCenter/ESXi event/log ingestion;
547 +- generated ESXi/VM/datastore vnodes;
548 +- sensitive identity labels such as guest IP, inventory path, MAC, IQN, WWN, and
549 + datastore paths;
550 +- per-child-instance VM/host metric families;
551 +- deeper vSAN internals beyond the shipped opt-in subset;
552 +- live vCenter permission probes in readiness;
553 +- context propagation through all govmomi calls.
554 +
555 +## Regression Log
556 +
557 +No active regression remains for the final shipped scope.
.agents/sow/specs/vsphere-parity-matrix.md new
+217
@@ -0,0 +1,217 @@
1 +# vSphere Collector Parity Matrix
2 +
3 +Status: draft baseline for `SOW-0015`.
4 +
5 +Purpose: normalize LogicMonitor, Datadog, and mirrored open-source vSphere
6 +coverage into Netdata implementation groups. Every row is classified; there are
7 +no `unknown` rows.
8 +
9 +Classification values:
10 +
11 +- `existing-default`: current Netdata vSphere collector already collects it by
12 + default.
13 +- `new-default`: safe additive object-level metric group planned for default
14 + collection in this SOW.
15 +- `opt-in`: implement only behind explicit config selectors/limits because of
16 + cardinality, cost, sensitivity, or reviewer scope.
17 +- `covered-elsewhere`: Netdata already has another collector/surface for it.
18 +- `follow-up`: valid requirement, but belongs to another ingestion path or SOW.
19 +- `non-metric-surface`: valid parity surface, but must be implemented through
20 + topology output or Functions, not as metric labels.
21 +- `out-of-scope-pr`: valid parity surface intentionally excluded from this PR by
22 + user decision.
23 +- `excluded`: intentionally not supported by default because it conflicts with
24 + product direction or would duplicate a better Netdata source.
25 +
26 +## Source Evidence
27 +
28 +Official docs checked on 2026-05-08:
29 +
30 +- LogicMonitor VMware vSphere Monitoring:
31 + `https://www.logicmonitor.com/support/vmware-vsphere-monitoring`
32 +- Datadog vSphere integration:
33 + `https://docs.datadoghq.com/integrations/vsphere/`
34 +- Broadcom vSphere Web Services API:
35 + - `VirtualMachineSnapshotInfo`
36 + - `VirtualMachineSnapshotTree`
37 + - `DatastoreSummary`
38 + - `PerformanceManager`
39 + - `VirtualDisk`
40 + - `VirtualEthernetCard`
41 + - `StoragePod`
42 + - `Network`
43 + - `NetworkSummary`
44 + - `DistributedVirtualPortgroup`
45 + - `HostHostBusAdapter`
46 + - `HostScsiDisk`
47 + - `HostMultipathInfo`
48 + - CPU, network, disk, virtual disk, storage adapter, storage path, and power
49 + performance-counter pages
50 +- Broadcom vSphere Automation API:
51 + - `Cis Tagging Tag Association`
52 +- Broadcom vSAN Management API:
53 + - API overview, managed objects, endpoints, and `VsanPerformanceManager`
54 +
55 +Mirrored repository evidence:
56 +
57 +Note: local mirrored repositories are shallow clones. Evidence below is
58 +snapshot-only evidence from the checked HEAD commit; it supports current-source
59 +parity comparisons, not history/blame/timeline conclusions.
60 +
61 +- `DataDog/integrations-core @ 1befb9012c44152b0aedfb17142041bcc9c1dc61`
62 + - `vsphere/datadog_checks/vsphere/data/conf.yaml.example:91`
63 + - `vsphere/datadog_checks/vsphere/data/conf.yaml.example:115`
64 + - `vsphere/datadog_checks/vsphere/data/conf.yaml.example:175`
65 + - `vsphere/datadog_checks/vsphere/data/conf.yaml.example:262`
66 + - `vsphere/datadog_checks/vsphere/data/conf.yaml.example:326`
67 + - `vsphere/datadog_checks/vsphere/data/conf.yaml.example:375`
68 + - `vsphere/datadog_checks/vsphere/data/conf.yaml.example:386`
69 + - `vsphere/datadog_checks/vsphere/metrics.py:74`
70 + - `vsphere/datadog_checks/vsphere/metrics.py:211`
71 + - `vsphere/datadog_checks/vsphere/metrics.py:413`
72 + - `vsphere/datadog_checks/vsphere/metrics.py:497`
73 +- `influxdata/telegraf @ 5a1147f1bb725ff8fd483ea55045506aa70db191`
74 + - `plugins/inputs/vsphere/README.md:43`
75 + - `plugins/inputs/vsphere/README.md:86`
76 + - `plugins/inputs/vsphere/README.md:145`
77 + - `plugins/inputs/vsphere/README.md:173`
78 + - `plugins/inputs/vsphere/README.md:187`
79 + - `plugins/inputs/vsphere/README.md:213`
80 + - `plugins/inputs/vsphere/vsphere.go:23`
81 + - `plugins/inputs/vsphere/vsphere.go:153`
82 + - `plugins/inputs/vsphere/vsan.go:41`
83 + - `plugins/inputs/vsphere/vsan.go:115`
84 + - `plugins/inputs/vsphere/vsan.go:201`
85 +- `grafana/vmware_exporter @ 3edc42190c6709567c0465304525f42ead2ac550`
86 + - `vsphere/test_metrics.txt:1`
87 + - `vsphere/test_metrics.txt:72`
88 + - `vsphere/test_metrics.txt:80`
89 +- `elastic/beats @ 7bbe8ee6dcfbf416c53ceb7725909d37a499846c`
90 + - `metricbeat/module/vsphere/virtualmachine/virtualmachine.go:79`
91 + - `metricbeat/module/vsphere/virtualmachine/virtualmachine.go:169`
92 + - `metricbeat/module/vsphere/virtualmachine/data.go:75`
93 + - `metricbeat/module/vsphere/datastorecluster/datastorecluster.go:46`
94 + - `metricbeat/module/vsphere/network/network.go`
95 +- `zabbix/zabbix @ bbf78e24c09c90ed9d18f1570b4fd2618981d72f`
96 + - `templates/app/vmware/template_app_vmware.yaml:1132`
97 + - `templates/app/vmware/template_app_vmware.yaml:1498`
98 + - `templates/app/vmware/template_app_vmware.yaml:1532`
99 + - `templates/app/vmware/template_app_vmware.yaml:1600`
100 + - `templates/app/vmware/template_app_vmware.yaml:1633`
101 + - `templates/app/vmware/template_app_vmware.yaml:1703`
102 + - `templates/app/vmware/template_app_vmware.yaml:1778`
103 + - `templates/app/vmware/template_app_vmware.yaml:4162`
104 + - `templates/app/vmware/template_app_vmware.yaml:4418`
105 +- `newrelic/nri-vsphere @ 9366fcd3d597ae0712c94882331042d74fe38e22`
106 + - `README.md:8`
107 + - `README.md:26`
108 + - `README.md:30`
109 + - `README.md:42`
110 + - `internal/collect/vms.go:21`
111 + - `internal/collect/vms.go:22`
112 + - `internal/collect/networks.go:14`
113 + - `internal/collect/networks.go:37`
114 + - `internal/process/datacenter.go:104`
115 + - `internal/process/hosts.go:95`
116 + - `internal/process/vms.go:113`
117 + - `vsphere-performance.metrics:22`
118 + - `vsphere-performance.metrics:140`
119 + - `test-data/README.md:1`
120 +- `open-telemetry/opentelemetry-collector-contrib @ 34ed18e037dc63e41c4b4a8356d2a13d55c768f4`
121 + - `receiver/vcenterreceiver/metadata.yaml:25`
122 + - `receiver/vcenterreceiver/metadata.yaml:75`
123 + - `receiver/vcenterreceiver/metadata.yaml:276`
124 + - `receiver/vcenterreceiver/metadata.yaml:434`
125 + - `receiver/vcenterreceiver/metadata.yaml:491`
126 + - `receiver/vcenterreceiver/metadata.yaml:689`
127 + - `receiver/vcenterreceiver/metadata.yaml:791`
128 + - `receiver/vcenterreceiver/resources.go:102`
129 + - `receiver/vcenterreceiver/internal/mockserver/README.md:1`
130 + - `receiver/vcenterreceiver/internal/mockserver/responses/cluster-vsan.xml`
131 +- `grafana/alloy @ c1b740cd7fc7d2b521304ee15c9c9f61d0d5ceb0`
132 + - `internal/component/otelcol/receiver/vcenter`
133 +
134 +## Matrix
135 +
136 +| ID | Surface | Main sources | Netdata target | Classification | Default policy and implementation requirements |
137 +|---|---|---|---|---|---|
138 +| P01 | VM aggregate CPU, memory, swap, disk IO, disk max latency, network traffic, packets, drops, overall alarm status, uptime | Current Netdata; LogicMonitor VM performance; Datadog VM metrics; Telegraf VM metrics | Existing contexts `vsphere.vm_*` in `vsphere-v1-compatibility-manifest.md` | `existing-default` | Preserve contexts, dimensions, labels, units, and sample keys exactly. Chart IDs intentionally change under framework V2 by user decision on 2026-05-08. Empty VM performance scrape results warn and continue with VM property/status metrics and later resource surfaces. |
139 +| P02 | ESXi host aggregate CPU, memory, swap, disk IO, disk max latency, network traffic, packets, drops, errors, overall alarm status, uptime | Current Netdata; LogicMonitor host performance; Datadog host metrics; Telegraf host metrics | Existing contexts `vsphere.host_*` | `existing-default` | Preserve current default collection. Empty host performance scrape results warn and continue with host property/status metrics and later resource surfaces. |
140 +| P03 | Datastore aggregate capacity/free/used/used percent, overall status, IO throughput, IOPS, latency | Current Netdata; LogicMonitor datastore usage/status/throughput; Datadog datastore metrics; Telegraf datastore metrics | Existing contexts `vsphere.datastore_*` | `existing-default` | Preserve datastore accessibility guard: capacity/free/used are trusted only when accessible. |
141 +| P04 | Cluster host count, CPU/memory capacity, CPU topology, DRS/HA enabled, overall status, vMotions, DRS score/balance, VM count, DRS usage summary, aggregate performance, VM operation counters | Current Netdata; LogicMonitor clusters; Datadog cluster metrics; Telegraf cluster metrics; Grafana exporter cluster metrics | Existing contexts `vsphere.cluster_*` | `existing-default` | Preserve property-vs-perf two-phase lifecycle. |
142 +| P05 | Resource pool CPU/memory usage, entitlement, allocation, memory breakdown, config, overall status | Current Netdata; LogicMonitor resource pools; Telegraf resource pools | Existing contexts `vsphere.resource_pool_*` | `existing-default` | Preserve current resource-pool property refresh behavior and labels. |
143 +| P06 | VM snapshot aggregate count, maximum snapshot age, maximum chain depth | User requirement; LogicMonitor VM snapshots; Zabbix snapshot count/latest date; Elastic snapshot info; New Relic optional snapshots; Broadcom snapshot API | Implemented contexts: `vsphere.vm_snapshot_count`, `vsphere.vm_snapshot_max_age`, `vsphere.vm_snapshot_max_chain_depth` with labels `id`, `datacenter`, `cluster`, `host`, `vm` | `new-default` | Object-level per VM. Emits zero for VMs with no snapshots. Does not emit snapshot name/description/ID labels by default. Unit: snapshots, seconds, snapshots. Tests: empty, sibling, nested, zero create time, old create time. |
144 +| P07 | VM snapshot health alerts | User requirement; LogicMonitor/Zabbix snapshot alerting surfaces | Implemented health templates on `vsphere.vm_snapshot_max_chain_depth` and `vsphere.vm_snapshot_max_age` | `new-default` | Warn when chain depth > 3. Critical when max age > 24h. Alert docs and metadata added with the metrics. |
145 +| P08 | Datastore `accessible`, `maintenanceMode`, `uncommitted`, `multipleHostAccess` | Broadcom `DatastoreSummary`; Datadog datastore properties; LogicMonitor datastore status/usage | Implemented contexts: `vsphere.datastore_accessibility_status`, `vsphere.datastore_maintenance_status`, `vsphere.datastore_multiple_host_access`; existing `vsphere.datastore_space_usage` adds `uncommitted` | `new-default` | Object-level per datastore. Preserves capacity/free/uncommitted guard: values are emitted as zero when inaccessible. Maintenance and multi-host access are state-set charts with `unknown` for omitted API values. Initial discovery now keeps inaccessible datastores as status-only resources and datastore perf scraping skips inaccessible datastores. |
146 +| P09 | VM power state, connection state, VMware tools running/version status, disk consolidation-needed status, configured CPU/memory/disk/NIC counts, aggregate storage usage, guest OS name | LogicMonitor VM status; Datadog property metrics; Zabbix tools/status; Elastic VM summary | Implemented contexts: `vsphere.vm_power_state`, `vsphere.vm_connection_state`, `vsphere.vm_tools_running_status`, `vsphere.vm_tools_version_status`, `vsphere.vm_consolidation_needed`, `vsphere.vm_config_cpu`, `vsphere.vm_config_memory`, `vsphere.vm_config_devices`, `vsphere.vm_storage_usage` | `new-default` | Object-level per VM. VMs returned by vSphere and kept by include selectors are discovered regardless of power state. Non-powered-on VMs are property/status/snapshot-only; real-time performance scraping skips them. No guest hostname/IP labels by default. Tools and connection values are bounded state-set dimensions. Guest OS name remains a non-default label/property candidate because it is a free-form string. |
147 +| P10 | Host connection state, power state, maintenance mode | LogicMonitor host status; Datadog property metrics | Implemented contexts: `vsphere.host_power_state`, `vsphere.host_connection_state`, `vsphere.host_maintenance_status` | `new-default` | Object-level per host. Hosts returned by vSphere and kept by include selectors are discovered regardless of power state. Non-powered-on hosts are property/status-only; real-time performance scraping skips them. Collector-generated ESXi vnode routing is excluded from this PR. |
148 +| P11 | Cluster DRS mode/vMotion rate and HA details beyond current enabled/admission-control booleans | Datadog property metrics; LogicMonitor HA/admission control | Implemented contexts: existing `vsphere.cluster_drs_config`, `vsphere.cluster_ha_config`; additive `vsphere.cluster_drs_mode`, `vsphere.cluster_drs_vmotion_rate`, `vsphere.cluster_ha_host_monitoring`, `vsphere.cluster_ha_vm_monitoring`, `vsphere.cluster_ha_vm_component_protection` | `new-default` | Object-level per cluster. Uses bounded state-set dimensions plus a numeric vMotion recommendation threshold. Does not expose free-form cluster config strings as labels. |
149 +| P12 | Datacenter object counts and inventory counts | LogicMonitor object count/info; Datadog datacenter metrics; Telegraf datacenter controls | Implemented context: `vsphere.inventory_objects` with datacenters, folders, clusters, hosts, VMs, datastores, and resource-pool dimensions after include filters are applied | `new-default` | Job-level aggregate metric, not a mandatory vCenter vnode. V2 uses only the static `id=inventory` instance label. |
150 +| P13 | Datastore clusters / storage pods capacity and usage | LogicMonitor datastore clusters; Broadcom `StoragePod`; Elastic datastorecluster module | Implemented optional contexts: `vsphere.datastore_cluster_space_utilization`, `vsphere.datastore_cluster_space_usage`, `vsphere.datastore_cluster_storage_drs_status` | `opt-in` | Default off. Enable with `collect_datastore_clusters`. `datastore_cluster_include` matches `/Datacenter/DatastoreCluster`, name, or managed object ID. Labels: `id`, `datacenter`, `datastore_cluster`. |
151 +| P14 | VM virtual disk capacity by disk/device | LogicMonitor VM disk capacity; Broadcom `VirtualDisk`; Zabbix VM storage; Datadog property/perf metrics; 2026-05-22 user decision | No per-virtual-disk capacity metrics in this PR | `out-of-scope-pr` | Hard-removed before merge by user decision. Existing aggregate VM disk I/O and max-latency contexts remain default-on. Reintroduce per-disk capacity only through a focused PR with a clear need and config contract. |
152 +| P15 | VM virtual disk performance by disk/device | Broadcom disk I/O counter docs; Datadog per-instance `virtualDisk.*`; Telegraf `virtualDisk.*`; OTel VM disk metrics; New Relic performance levels; 2026-05-22 user decision | No per-virtual-disk performance metrics in this PR | `out-of-scope-pr` | Hard-removed before merge by user decision. Existing aggregate VM disk I/O and max-latency contexts remain default-on. Reintroduce per-disk performance only through a focused PR with a clear need and config contract. |
153 +| P16 | VM network interface throughput/packets/errors/drops by NIC | Broadcom latest network counters; Datadog per-instance VM `net.*`; Telegraf VM instance metrics; OTel VM vNIC metrics; LogicMonitor VM interface; 2026-05-22 user decision | No per-VM-network-interface metrics in this PR | `out-of-scope-pr` | Hard-removed before merge by user decision. Existing aggregate VM network traffic, packets, and drops contexts remain default-on. Reintroduce per-vNIC performance only through a focused PR with a clear need and config contract. |
154 +| P17 | Host physical NIC metrics by NIC | Broadcom latest network counters; LogicMonitor ESXi network interfaces; Telegraf host instances; Datadog per-instance host metrics; OTel host pNIC metrics; 2026-05-22 user decision | No per-host-physical-NIC metrics in this PR | `out-of-scope-pr` | Hard-removed before merge by user decision because it is a high-cardinality host child-instance surface. Existing aggregate ESXi host network metrics remain default-on. Reintroduce per-pNIC performance only through a focused PR with a clear need and config contract. |
155 +| P18 | Host disk/LUN/device metrics by disk/device | Broadcom disk counters and `HostScsiDisk`; LogicMonitor ESXi disks; Datadog per-instance disk metrics; Telegraf host disk metrics; OTel host disk metrics; 2026-05-22 user decision | No per-host-disk/LUN/device metrics in this PR | `out-of-scope-pr` | Hard-removed before merge by user decision because it is a high-cardinality host child-instance surface. Existing aggregate ESXi host disk metrics remain default-on. Reintroduce per-device performance only through a focused PR with a clear need and config contract. |
156 +| P19 | Host storage adapter metrics | Broadcom storage adapter counters and `HostHostBusAdapter`; Datadog/Telegraf/New Relic storageAdapter metrics; 2026-05-22 user decision | No per-host-storage-adapter metrics in this PR | `out-of-scope-pr` | Hard-removed before merge by user decision because it is a high-cardinality host child-instance surface. Reintroduce storage-adapter performance only through a focused PR with a clear need and config contract. |
157 +| P20 | Host storage path metrics | Broadcom storage path counters and `HostMultipathInfo`; Datadog/Telegraf/New Relic storagePath metrics; 2026-05-22 user decision | No per-host-storage-path metrics in this PR | `out-of-scope-pr` | Hard-removed before merge by user decision because it is a high-cardinality host child-instance surface. Reintroduce storage-path performance only through a focused PR with a clear need and config contract. |
158 +| P21 | Host CPU core/thread/logical processor metrics | Broadcom CPU counters; LogicMonitor logical processors; Telegraf host CPU instances; New Relic host CPU instance counters; OTel idle CPU metric; 2026-05-22 user decision | No per-host-CPU-instance metrics in this PR | `out-of-scope-pr` | Hard-removed before merge by user decision because it is a high-cardinality host child-instance surface. Existing aggregate ESXi host CPU metrics remain default-on. Reintroduce per-CPU-instance performance only through a focused PR with a clear need and config contract. |
159 +| P22 | Host and VM power/energy counters | Broadcom power counters; LogicMonitor ESXi power; Datadog/Telegraf/New Relic power counters; 2026-05-22 user decision | Implemented contexts: `vsphere.host_power_usage`, `vsphere.host_power_capacity_usage`, `vsphere.host_power_capacity_utilization`, `vsphere.host_energy_usage`, `vsphere.vm_power_usage`, `vsphere.vm_energy_usage` | `implemented` | `collect_power_metrics` was removed before merge by user decision. The collector requests aggregate vSphere `power.*` counters with empty instance for discovered powered-on hosts and VMs when vSphere exposes those counters. No child selector is added because this emits one aggregate set per included host/VM. Host labels: `id`, `datacenter`, `cluster`, `host`, plus opt-in enrichment labels. VM labels: `id`, `datacenter`, `cluster`, `host`, `vm`, plus opt-in enrichment labels. |
160 +| P23 | Hardware health sensors: fans, power, storage, memory, processor, system sensors | LogicMonitor ESXi hardware/system sensors; SNMP `vmware-esx` profile | SNMP profile `vmware-esx`; optional future direct vSphere/CIM metrics | `covered-elsewhere` | Do not duplicate by default. vSphere docs point users to `snmp` with `vmware-esx`; if direct API sensors are added later, make opt-in and map against SNMP coverage. |
161 +| P24 | ESXi HBA/environment/hardware health via SNMP | Netdata SNMP `vmware-esx` overlap | SNMP `vmware-esx` profile | `covered-elsewhere` | vSphere docs mention this as complementary per-host monitoring. |
162 +| P25 | vCenter Server Appliance CPU, memory, disk, filesystem, services, health, VCHA, backup | LogicMonitor VCSA modules | Netdata `vcsa` collector | `covered-elsewhere` | Keep out of this vSphere collector. vSphere docs point users to `vcsa` for appliance health. |
163 +| P26 | vSAN cluster, host, and VM capacity/performance/health/events | Broadcom vSAN Management API; Datadog vSAN metrics/events; Telegraf vSAN controls; OTel vSAN metrics/fixtures | Implemented opt-in contexts: `vsphere.vsan_cluster_space_usage`, `vsphere.vsan_cluster_space_utilization`, `vsphere.vsan_cluster_health_status`, `vsphere.vsan_cluster_operations`, `vsphere.vsan_cluster_throughput`, `vsphere.vsan_cluster_latency`, `vsphere.vsan_cluster_congestions`, `vsphere.vsan_host_operations`, `vsphere.vsan_host_throughput`, `vsphere.vsan_host_latency`, `vsphere.vsan_host_congestions`, `vsphere.vsan_host_cache_hit_rate`, `vsphere.vsan_vm_operations`, `vsphere.vsan_vm_throughput`, `vsphere.vsan_vm_latency` | `opt-in` | Default off. Enable with `collect_vsan`. Uses vSAN Management API only for vSAN-enabled clusters that pass `vsan_cluster_include`; host and VM vSAN performance entities pass `vsan_host_include` and `vsan_vm_include`. Emits cluster space/health and the OTel/Datadog common cluster/host/VM vSAN performance subset. vSAN events are excluded with P27. Deeper vSAN disk-group, disk, component, CMMDS, and all Telegraf entity-type metrics remain a residual parity gap because they need explicit Netdata NIDL/context mapping and bounded config policy before implementation. |
164 +| P27 | vCenter and ESXi events, alarms, event filters | Datadog events; LogicMonitor LogSources; New Relic optional events | Future logs/events ingestion, likely OTEL/log path | `out-of-scope-pr` | User decision 2026-05-08: do not implement events in this PR. Do not overload metric collector. |
165 +| P28 | vSphere tags and custom attributes as labels | Broadcom Tag Association API and CustomFieldsManager; Datadog tags/attributes; Telegraf custom attributes; Elastic custom fields; New Relic tags | Implemented opt-in labels `vsphere_tag_<sanitized_category>` and `vsphere_custom_attribute_<sanitized_name>` | `opt-in` | Default off. `tag_categories` allowlists vSphere tag categories with one glob pattern per YAML list item; multiple tags in one category are sorted and joined with the pipe character. `custom_attributes` allowlists custom attributes with one glob pattern per YAML list item. Discovery fails open with warnings if tag/custom-attribute APIs are unavailable. |
166 +| P29 | Inventory paths, folder lineage, guest hostnames, guest IPs, MAC/IQN/WWN ERI-like identity | LogicMonitor topology/ERI/netscan; Datadog filters; Telegraf IP addresses; OTel inventory-path resource attributes; Broadcom guest/device/storage objects | No metric labels implemented in this PR | `out-of-scope-pr` | User decision 2026-05-20: remove inventory-path and VM guest hostname/IP/OS labels before merge. Remaining MAC/IQN/WWN/device identity also remains out of scope because these are sensitive multi-value identity surfaces that need a separate product decision and bounded policy. |
167 +| P30 | Topology edges: cluster, datastore, network, VM topology | LogicMonitor topology sources; Elastic network/datastorecluster modules; OTel resource model; New Relic object fixtures; Netdata Function topology schema and SNMP topology pattern | Implemented public `topology:vsphere` cached Function alias with required job selector | `non-metric-surface` | Uses cached discovery state and emits topology actors/links, not metric labels. Default inventory topology includes datacenters, clusters, hosts, VMs, datastores, datastore clusters, and resource pools. Network actors and host/VM network links are included only when `collect_network_topology` is enabled. Canonical framework registration is also available as `vsphere:topology:vsphere`; topology consumers should use `topology:vsphere`. |
168 +| P31 | Network and distributed virtual port group status | LogicMonitor network state; Elastic network module; New Relic network object fixtures; Broadcom `NetworkSummary.accessible`; Broadcom `DistributedVirtualPortgroup` | Implemented opt-in cached topology actors through `collect_network_topology` | `non-metric-surface` | Default off to avoid extra vCenter discovery calls for existing users. When enabled, discovers vSphere `Network` objects, including distributed virtual port groups returned by the Network view, and exposes cached `accessible`, `overall_status`, type, host count, VM count, and host/VM links in the topology Function. Does not create charts, metrics, or free-form network path labels. |
169 +| P32 | Troubleshooter and permission/readiness checks | LogicMonitor troubleshooter; Datadog service checks; Netdata Function table schema | Implemented `vsphere:readiness` cached Function with required job selector | `non-metric-surface` | Read-only cached Function reporting target/credential presence, initialized client/discovery/performance-counter state, inventory counts, optional metric/label gates, network topology gate, and cached vSAN counts. It does not expose configured URL/credentials and does not issue extra vCenter API calls. Live permission probes remain intentionally absent from this PR. |
170 +| P33 | VM vnodes | User discussion; Netdata agent-on-VM duplication risk; V2 host-scope spec; 2026-05-20 review decision | No collector-generated VM vnodes in this PR | `out-of-scope-pr` | Hard-removed before merge. Reintroduce only through a focused PR with stable VM identity, lifecycle, docs, and duplicate-node policy. |
171 +| P34 | ESXi vnodes | User decision; V2 host-scope spec; 2026-05-20 review decision | No collector-generated ESXi vnodes in this PR | `out-of-scope-pr` | Hard-removed before merge. Reintroduce only through a focused PR with stable ESXi identity and lifecycle policy. |
172 +| P35 | Datastore vnodes | User decision and cardinality/identity review | No datastore vnodes in this SOW | `excluded` | Keep datastores as metric instances, not nodes. Revisit only with product decision. |
173 +| P36 | TKG/Kubernetes workload metrics inside vSphere VMs | Datadog TKG note; Netdata Kubernetes collectors/agents | Netdata Agent and Kubernetes collectors inside guest/cluster | `covered-elsewhere` | Do not collect container/pod/node workload metrics through vSphere. |
174 +
175 +## Implementation Gates Derived From Matrix
176 +
177 +Before framework v2 migration:
178 +
179 +- Preserve all `existing-default` rows through the compatibility manifest.
180 +- Add no `new-default` row until the v2 migration passes the manifest.
181 +
182 +For `new-default` rows:
183 +
184 +- Add one resource type per context.
185 +- Use only stable state dimensions and numeric dimensions.
186 +- Do not add sensitive labels by default.
187 +- Add docs, metadata, config schema, stock config, and health alert updates in
188 + the same commit group.
189 +
190 +For `opt-in` rows:
191 +
192 +- Each row needs a config knob and selector/allowlist when the surface can
193 + emit child-instance or user-defined labels. User decision on 2026-05-20
194 + removed all `max_*` knobs from this collector; selectors and allowlists are
195 + the controls for optional resource instances and user-defined labels.
196 +- Default must be off.
197 +- Tests must cover both disabled and enabled behavior.
198 +
199 +For `covered-elsewhere` rows:
200 +
201 +- Update vSphere docs to explain the complementary collector.
202 +- Do not duplicate default data unless a later SOW records a product decision.
203 +
204 +For `follow-up` rows:
205 +
206 +- No remaining row should use `follow-up` in this PR unless the user explicitly
207 + asks to split it again.
208 +
209 +For `non-metric-surface` rows:
210 +
211 +- Implement through topology output or Functions only.
212 +- Do not add topology/resource identity as metric labels unless that label is
213 + separately covered by an opt-in label-enrichment row.
214 +
215 +For `out-of-scope-pr` rows:
216 +
217 +- Record the user decision and do not implement in this PR.
.agents/sow/specs/vsphere-v1-compatibility-manifest.md new
+503
@@ -0,0 +1,503 @@
1 +# vSphere Collector V1 Compatibility Manifest
2 +
3 +Status: superseded historical baseline for `SOW-0015`.
4 +
5 +The executable V1 golden fixture
6 +`src/go/plugin/go.d/collector/vsphere/testdata/v1_compat_manifest.json` and
7 +`TestCollector_V1CompatibilityManifest` were removed on 2026-05-22 with the
8 +runtime chart bridge cleanup. Current executable coverage is provided by
9 +`TestCollector_ChartTemplateYAML`, `TestCollector_V2CompatibilitySurface`,
10 +`collecttest.AssertChartCoverage`, feature-specific V2 plan tests, and full
11 +vSphere collector tests.
12 +
13 +This file is retained as migration-history evidence. Tables below that describe
14 +pre-merge experiments removed by later user decisions are historical, not the
15 +current accepted configuration or metric surface.
16 +
17 +This manifest records the pre-migration v1 contract that guides the framework v2 migration.
18 +The migration preserves contexts, dimensions, old labels, units, configuration,
19 +and metric meaning. Chart IDs are recorded for traceability but intentionally
20 +change to normal framework V2 instance chart IDs by user decision on 2026-05-08.
21 +The V2 chartengine path adds one new instance label, `id`, set to the vSphere
22 +managed-object reference that V1 used as the chart-ID prefix.
23 +
24 +Runtime substitution:
25 +
26 +- `%s` in chart and dimension IDs is the vSphere managed object reference value
27 + stored as the Netdata resource ID, for example synthetic `vcsim` IDs such as
28 + `vm-62`, `host-21`, `datastore-59`, `domain-c28`, or `resgroup-27`.
29 +- Default chart type is `line` when the code does not set `Type`.
30 +- Default dimension algorithm is `absolute`; default multiplier and divisor are
31 + `1`.
32 +
33 +## Sources
34 +
35 +- `src/go/plugin/go.d/collector/vsphere/collector.go`
36 +- `src/go/plugin/go.d/collector/vsphere/charts.yaml` (current chart source of truth; the transitional Go chart mirror was removed on 2026-05-22)
37 +- `src/go/plugin/go.d/collector/vsphere/collect.go`
38 +- `src/go/plugin/go.d/collector/vsphere/discover/metric_lists.go`
39 +- `src/go/plugin/go.d/collector/vsphere/config_schema.json`
40 +- `src/go/plugin/go.d/collector/vsphere/metadata.yaml`
41 +- `src/go/plugin/go.d/config/go.d/vsphere.conf`
42 +- `src/health/health.d/vsphere.conf`
43 +- `src/go/plugin/go.d/collector/vsphere/collector_test.go`
44 +
45 +Removed executable golden scope:
46 +
47 +- the deleted golden fixture recorded V1 chart IDs and pinned contexts, titles,
48 + units, families, chart types, priorities, label keys, label sources,
49 + dimension names/algorithms/scales/options, and metric sample keys/values from
50 + `vcsim`;
51 +- it did not pin simulator-specific label values because `vcsim` can assign VM
52 + runtime host labels differently between runs;
53 +- it is no longer regenerated. Current V2 validation relies on chart-template
54 + generation, chartengine materialization, metric-store coverage, and focused
55 + feature tests instead of a large V1 runtime-chart golden.
56 +
57 +## Collector Registration And Defaults
58 +
59 +| Field | Current v1 contract |
60 +|---|---|
61 +| Module | `vsphere` |
62 +| Framework registration baseline | Pre-migration `Create`, returning `collectorapi.CollectorV1`; current implementation registers `CreateV2` while preserving this public chart/metric surface except chart IDs. |
63 +| Default `update_every` | `20` seconds |
64 +| Default HTTP timeout | `20s` |
65 +| Default discovery interval | `5m` |
66 +| Default host include | `/*` |
67 +| Default VM include | `/*` |
68 +| Default datastore include | `/*` |
69 +| Default cluster include | `/*` |
70 +| Default inventory path label | `false` |
71 +| Default VM guest labels | empty allowlist |
72 +| Default vSphere tag category labels | empty allowlist |
73 +| Default custom attribute labels | empty allowlist |
74 +| Default datastore cluster collection | `false` |
75 +| Default datastore cluster include | `/*` |
76 +| Default host NIC performance collection | `false` |
77 +| Default host NIC include | `*` |
78 +| Default host disk performance collection | `false` |
79 +| Default host disk include | `*` |
80 +| Default host storage adapter performance collection | `false` |
81 +| Default host storage adapter include | `*` |
82 +| Default host storage path performance collection | `false` |
83 +| Default host storage path include | `*` |
84 +| Default host CPU instance performance collection | `false` |
85 +| Default host CPU instance include | `*` |
86 +| Default vSAN collection | `false` |
87 +| Default network topology collection | `false` |
88 +| Collection output baseline | Pre-migration `Collect(context.Context) map[string]int64`; current public collection path writes to the framework V2 metric store. |
89 +| Config schema embed | `config_schema.json` |
90 +
91 +## Historical Configuration Contract
92 +
93 +This table records the migration baseline before later cleanup decisions. It is
94 +not the authoritative current accepted surface after the 2026-05-20 and
95 +2026-05-22 removals documented in `SOW-0015`.
96 +
97 +| YAML key | JSON key | Required by schema | Notes |
98 +|---|---|---:|---|
99 +| `vnode` | `vnode` | no | Existing job-level vnode association. |
100 +| `update_every` | `update_every` | no | Default `20`. |
101 +| `autodetection_retry` | `autodetection_retry` | no | Schema default `60`; metadata lists `0`. Preserve accepted key. |
102 +| `url` | `url` | yes | From embedded HTTP config. |
103 +| `timeout` | `timeout` | no | From embedded HTTP config. |
104 +| `discovery_interval` | `discovery_interval` | no | Minimum `60`, default `300`. |
105 +| `not_follow_redirects` | `not_follow_redirects` | no | From embedded HTTP config. |
106 +| `host_include` | `host_include` | no | Selector list, default `/*`. |
107 +| `vm_include` | `vm_include` | no | Selector list, default `/*`. |
108 +| `datastore_include` | `datastore_include` | no | Selector list, default `/*`. |
109 +| `cluster_include` | `cluster_include` | no | Selector list, default `/*`. |
110 +| `tag_categories` | `tag_categories` | no | Optional vSphere tag category label allowlist. Default empty; each YAML list item is one glob pattern matching a tag category name. Matching categories become labels named `vsphere_tag_<sanitized_category>`; multiple tags in one category are sorted and joined with the pipe character. |
111 +| `custom_attributes` | `custom_attributes` | no | Optional vSphere custom attribute label allowlist. Default empty; each YAML list item is one glob pattern matching a custom attribute name. Matching attributes become labels named `vsphere_custom_attribute_<sanitized_name>`. |
112 +| `collect_datastore_clusters` | `collect_datastore_clusters` | no | Optional datastore cluster / StoragePod metrics. Default `false`. |
113 +| `datastore_cluster_include` | `datastore_cluster_include` | no | Optional simple-pattern allowlist for datastore clusters. Default `/*`; matches `/Datacenter/DatastoreCluster`, datastore-cluster name, or managed object ID. |
114 +| `collect_vsan` | `collect_vsan` | no | Optional vSAN metrics. Default `false`; requests vSAN cluster space/health and vSAN cluster, host, and VM performance metrics through the vSAN Management API for vSAN-enabled clusters. |
115 +| `vsan_cluster_include` | `vsan_cluster_include` | no | Optional simple-pattern allowlist for vSAN-enabled clusters. Default `/*`; matches `/Datacenter/Cluster`, cluster name, managed object ID, or `vsan_uuid:<uuid>`. |
116 +| `vsan_host_include` | `vsan_host_include` | no | Optional simple-pattern allowlist for vSAN host performance entities. Default `/*`; matches `/Datacenter/Cluster/Host`, host name, managed object ID, or `vsan_node_uuid:<uuid>`. |
117 +| `vsan_vm_include` | `vsan_vm_include` | no | Optional simple-pattern allowlist for vSAN VM performance entities. Default `/*`; matches `/Datacenter/Cluster/Host/VM`, VM name, managed object ID, or `instance_uuid:<uuid>`. |
118 +| `collect_network_topology` | `collect_network_topology` | no | Optional vSphere Network discovery for the cached topology Function. Default `false`; discovers Network/Distributed Virtual Port Group objects for topology only and emits no charts or metrics. |
119 +| `username` | `username` | yes | Sensitive. |
120 +| `password` | `password` | yes | Sensitive. |
121 +| `bearer_token_file` | `bearer_token_file` | no | Hidden in UI schema. |
122 +| `force_http2` | `force_http2` | no | Hidden in UI schema. |
123 +| `proxy_url` | `proxy_url` | no | From embedded HTTP config. |
124 +| `proxy_username` | `proxy_username` | no | Sensitive. |
125 +| `proxy_password` | `proxy_password` | no | Sensitive. |
126 +| `headers` | `headers` | no | Object or null. |
127 +| `tls_skip_verify` | `tls_skip_verify` | no | From embedded HTTP config. |
128 +| `tls_ca` | `tls_ca` | no | Absolute path or empty. |
129 +| `tls_cert` | `tls_cert` | no | Absolute path or empty. |
130 +| `tls_key` | `tls_key` | no | Absolute path or empty. |
131 +| `body` | `body` | no | Hidden in UI schema. |
132 +| `method` | `method` | no | Hidden in UI schema. |
133 +
134 +Stock config examples:
135 +
136 +- `vcenter1`: `url`, `username`, `password`
137 +- `vcenter2`: `url`, `username`, `password`
138 +- The stock config also documents selector formats, secret resolver syntax,
139 + optional `vnode`, safe discovery defaults, and every valid host/VM power
140 + state as commented examples.
141 +
142 +## Label Contract
143 +
144 +| Resource | Labels and value sources |
145 +|---|---|
146 +| Inventory | V2 adds `id=inventory`; no old V1 labels |
147 +| VM | `datacenter=vm.Hier.DC.Name`, `cluster=getVMClusterName(vm)`, `host=vm.Hier.Host.Name`, `vm=vm.Name`; V2 also adds `id=vm.ID` |
148 +| Host | `datacenter=host.Hier.DC.Name`, `cluster=getHostClusterName(host)`, `host=host.Name`; V2 also adds `id=host.ID` |
149 +| Datastore | `datacenter=ds.Hier.DC.Name`, `datastore=ds.Name`, `type=ds.Type`; V2 also adds `id=ds.ID` |
150 +| Cluster | `datacenter=cl.Hier.DC.Name`, `cluster=cl.Name`; V2 also adds `id=cl.ID` |
151 +| Datastore cluster | `id=pod.ID`, `datacenter=pod.Hier.DC.Name`, `datastore_cluster=pod.Name` |
152 +| Host physical network interface performance | `id=host.ID`, `datacenter=host.Hier.DC.Name`, `cluster=host.Hier.Cluster.Name`, `host=host.Name`, `interface=<vSphere-performance-instance>`, `interface_instance=<vSphere-performance-instance>` |
153 +| Host disk/LUN/device performance | `id=host.ID`, `datacenter=host.Hier.DC.Name`, `cluster=host.Hier.Cluster.Name`, `host=host.Name`, `disk=<vSphere-performance-instance>`, `disk_instance=<vSphere-performance-instance>` |
154 +| Host storage adapter performance | `id=host.ID`, `datacenter=host.Hier.DC.Name`, `cluster=host.Hier.Cluster.Name`, `host=host.Name`, `adapter=<vSphere-performance-instance>`, `adapter_instance=<vSphere-performance-instance>` |
155 +| Host storage adapter aggregate performance | `id=host.ID`, `datacenter=host.Hier.DC.Name`, `cluster=host.Hier.Cluster.Name`, `host=host.Name` |
156 +| Host storage path performance | `id=host.ID`, `datacenter=host.Hier.DC.Name`, `cluster=host.Hier.Cluster.Name`, `host=host.Name`, `path=<vSphere-performance-instance>`, `path_instance=<vSphere-performance-instance>` |
157 +| Host storage path aggregate performance | `id=host.ID`, `datacenter=host.Hier.DC.Name`, `cluster=host.Hier.Cluster.Name`, `host=host.Name` |
158 +| Host CPU instance performance | `id=host.ID`, `datacenter=host.Hier.DC.Name`, `cluster=host.Hier.Cluster.Name`, `host=host.Name`, `cpu=<vSphere-performance-instance>`, `cpu_instance=<vSphere-performance-instance>` |
159 +| vSAN cluster | `id=cluster.ID`, `datacenter=cluster.Hier.DC.Name`, `cluster=cluster.Name`, `vsan_uuid=cluster.VSANUUID` |
160 +| vSAN host | `id=host.ID`, `datacenter=host.Hier.DC.Name`, `cluster=getHostClusterName(host)`, `host=host.Name`, `vsan_node_uuid=host.VSANNodeUUID` |
161 +| vSAN VM | `id=vm.ID`, `datacenter=vm.Hier.DC.Name`, `cluster=getVMClusterName(vm)`, `host=vm.Hier.Host.Name`, `vm=vm.Name`, `vm_instance_uuid=vm.InstanceUUID` |
162 +| Resource pool | `datacenter=rp.Hier.DC.Name`, `cluster=rp.Hier.Cluster.Name`, `resource_pool=rp.Name`; V2 also adds `id=rp.ID` |
163 +
164 +Compatibility details:
165 +
166 +- `getVMClusterName()` returns an empty string when the VM cluster name equals
167 + the host name.
168 +- `getHostClusterName()` returns an empty string when the host cluster name
169 + equals the host name.
170 +- By default, no per-resource V2 host scopes are created; all metrics follow
171 + the current job/global host behavior, with optional job-level `vnode`.
172 +- Collector-generated ESXi/VM vnodes are excluded from this PR by user decision
173 + on 2026-05-20. Reintroducing them requires a separate design for stable
174 + resource identity and host-scope lifecycle.
175 +- Empty host or VM real-time performance scrape results do not abort the whole
176 + collection cycle. The collector logs a warning and still emits available
177 + property/status metrics plus the remaining datastore, cluster, resource-pool,
178 + and vSAN surfaces.
179 +- Optional `tag_categories` and `custom_attributes` add user-defined
180 + vSphere metadata labels to VM, host, datastore, cluster, resource-pool, and
181 + datastore-cluster metrics when the matching resource has those values.
182 + Labels use sanitized keys prefixed with `vsphere_tag_` or
183 + `vsphere_custom_attribute_`. These labels are default-off because they may
184 + expose ownership, business, environment, or internal naming data.
185 +
186 +## Function Contract
187 +
188 +- `vsphere:readiness` is a read-only module Function with the framework job
189 + selector parameter. It reports cached collector readiness rows for
190 + target/credential presence, client/discovery/performance-counter state,
191 + inventory counts, optional metric/label gates, network-topology gate,
192 + and cached vSAN counts. It does not expose the configured vCenter URL,
193 + username, password, or object inventory names, and it does not issue extra
194 + vCenter API calls.
195 +- `topology:vsphere` is the public read-only topology Function alias with the
196 + framework job selector parameter and response type `topology`. It reports
197 + cached inventory actors and links for datacenters, clusters, ESXi hosts, VMs,
198 + datastores, datastore clusters, and resource pools. When
199 + `collect_network_topology` is enabled, it also includes cached vSphere
200 + Network/Distributed Virtual Port Group actors, accessibility/status
201 + attributes, and host/VM network links. The canonical framework method also
202 + registers as `vsphere:topology:vsphere`, but topology consumers should use
203 + `topology:vsphere` to match the existing topology Function convention.
204 +- Function surfaces are additive and do not change metric contexts,
205 + dimensions, chart templates, default host scopes, or existing configuration
206 + behavior.
207 +
208 +## Chart And Dimension Contract
209 +
210 +| Resource | Lifecycle | Chart ID template | Context | Family | Units | Type | Priority constant | Dimensions |
211 +|---|---|---|---|---|---|---|---|---|
212 +| Inventory | static job-level chart from collector initialization | `inventory_objects` | `vsphere.inventory_objects` | `inventory` | `objects` | `line` | `prioInventoryObjects` | `inventory_datacenters=>datacenters`; `inventory_folders=>folders`; `inventory_clusters=>clusters`; `inventory_hosts=>hosts`; `inventory_vms=>vms`; `inventory_datastores=>datastores`; `inventory_resource_pools=>resource_pools` |
213 +| Datastore cluster | optional charted only when `collect_datastore_clusters` is enabled and the StoragePod matches `datastore_cluster_include` | `datastore_cluster_space_utilization` | `vsphere.datastore_cluster_space_utilization` | `datastore clusters space` | `percentage` | `line` | optional V2 template | `datastore_cluster_space_utilization_used=>used div=100` |
214 +| Datastore cluster | optional charted only when `collect_datastore_clusters` is enabled and the StoragePod matches `datastore_cluster_include` | `datastore_cluster_space_usage` | `vsphere.datastore_cluster_space_usage` | `datastore clusters space` | `bytes` | `line` | optional V2 template | `datastore_cluster_space_usage_capacity=>capacity`; `datastore_cluster_space_usage_free=>free`; `datastore_cluster_space_usage_used=>used` |
215 +| Datastore cluster | optional charted only when `collect_datastore_clusters` is enabled and the StoragePod matches `datastore_cluster_include` | `datastore_cluster_storage_drs_status` | `vsphere.datastore_cluster_storage_drs_status` | `datastore clusters status` | `status` | `line` | optional V2 template | `datastore_cluster_storage_drs_status_enabled=>enabled`; `datastore_cluster_storage_drs_status_disabled=>disabled` |
216 +| vSAN cluster | optional charted only when `collect_vsan` is enabled and vSAN space usage is returned for a vSAN-enabled cluster | `vsan_cluster_space_utilization` | `vsphere.vsan_cluster_space_utilization` | `vSAN clusters space` | `percentage` | `line` | optional V2 template | `vsan_cluster_space_utilization_used=>used div=100` |
217 +| vSAN cluster | optional charted only when `collect_vsan` is enabled and vSAN space usage is returned for a vSAN-enabled cluster | `vsan_cluster_space_usage` | `vsphere.vsan_cluster_space_usage` | `vSAN clusters space` | `bytes` | `stacked` | optional V2 template | `vsan_cluster_space_usage_used=>used`; `vsan_cluster_space_usage_free=>free`; `vsan_cluster_space_usage_total=>total hidden` |
218 +| vSAN cluster | optional charted only when `collect_vsan` is enabled and vSAN health is returned for a vSAN-enabled cluster | `vsan_cluster_health_status` | `vsphere.vsan_cluster_health_status` | `vSAN clusters space` | `status` | `line` | optional V2 template | `vsan_cluster_health_status_green=>green`; `vsan_cluster_health_status_yellow=>yellow`; `vsan_cluster_health_status_red=>red`; `vsan_cluster_health_status_unknown=>unknown` |
219 +| vSAN cluster performance | optional charted only when `collect_vsan` is enabled and vSAN cluster performance is returned | `vsan_cluster_operations`; `vsan_cluster_throughput`; `vsan_cluster_latency`; `vsan_cluster_congestions` | `vsphere.vsan_cluster_operations`; `vsphere.vsan_cluster_throughput`; `vsphere.vsan_cluster_latency`; `vsphere.vsan_cluster_congestions` | `vSAN clusters performance` | `operations/s`; `bytes/s`; `microseconds`; `congestions/s` | `line`/`area` | optional V2 template | read/write operations, read/write throughput, read/write latency, congestions |
220 +| vSAN host performance | optional charted only when `collect_vsan` is enabled and vSAN host performance is returned | `vsan_host_operations`; `vsan_host_throughput`; `vsan_host_latency`; `vsan_host_congestions`; `vsan_host_cache_hit_rate` | `vsphere.vsan_host_operations`; `vsphere.vsan_host_throughput`; `vsphere.vsan_host_latency`; `vsphere.vsan_host_congestions`; `vsphere.vsan_host_cache_hit_rate` | `vSAN hosts performance` | `operations/s`; `bytes/s`; `microseconds`; `congestions/s`; `percentage` | `line`/`area` | optional V2 template | read/write operations, read/write throughput, read/write latency, congestions, hit_rate |
221 +| vSAN VM performance | optional charted only when `collect_vsan` is enabled and vSAN VM performance is returned | `vsan_vm_operations`; `vsan_vm_throughput`; `vsan_vm_latency` | `vsphere.vsan_vm_operations`; `vsphere.vsan_vm_throughput`; `vsphere.vsan_vm_latency` | `vSAN VMs performance` | `operations/s`; `bytes/s`; `microseconds` | `line`/`area` | optional V2 template | read/write operations, read/write throughput, read/write latency |
222 +| VM | property/perf charted when VM is discovered | `%s_cpu_utilization` | `vsphere.vm_cpu_utilization` | `vms cpu` | `percentage` | `line` | `prioVMCPUUtilization` | `%s_cpu.usage.average=>used div=100` |
223 +| VM | property/perf charted when VM is discovered | `%s_mem_utilization` | `vsphere.vm_mem_utilization` | `vms mem` | `percentage` | `line` | `prioVmMemoryUtilization` | `%s_mem.usage.average=>used div=100` |
224 +| VM | property/perf charted when VM is discovered | `%s_mem_usage` | `vsphere.vm_mem_usage` | `vms mem` | `KiB` | `line` | `prioVmMemoryUsage` | `%s_mem.granted.average=>granted`; `%s_mem.consumed.average=>consumed`; `%s_mem.active.average=>active`; `%s_mem.shared.average=>shared` |
225 +| VM | property/perf charted when VM is discovered | `%s_mem_swap_usage` | `vsphere.vm_mem_swap_usage` | `vms mem` | `KiB` | `line` | `prioVmMemorySwapUsage` | `%s_mem.swapped.average=>swapped` |
226 +| VM | property/perf charted when VM is discovered | `%s_mem_swap_io_rate` | `vsphere.vm_mem_swap_io` | `vms mem` | `KiB/s` | `area` | `prioVmMemorySwapIO` | `%s_mem.swapinRate.average=>in`; `%s_mem.swapoutRate.average=>out` |
227 +| VM | property/perf charted when VM is discovered | `%s_disk_io` | `vsphere.vm_disk_io` | `vms disk` | `KiB/s` | `area` | `prioVmDiskIO` | `%s_disk.read.average=>read`; `%s_disk.write.average=>write mul=-1` |
228 +| VM | property/perf charted when VM is discovered | `%s_disk_max_latency` | `vsphere.vm_disk_max_latency` | `vms disk` | `milliseconds` | `line` | `prioVmDiskMaxLatency` | `%s_disk.maxTotalLatency.latest=>latency` |
229 +| VM | property/perf charted when VM is discovered | `%s_net_traffic` | `vsphere.vm_net_traffic` | `vms net` | `KiB/s` | `area` | `prioVmNetworkTraffic` | `%s_net.bytesRx.average=>received`; `%s_net.bytesTx.average=>sent mul=-1` |
230 +| VM | property/perf charted when VM is discovered | `%s_net_packets` | `vsphere.vm_net_packets` | `vms net` | `packets` | `line` | `prioVmNetworkPackets` | `%s_net.packetsRx.summation=>received`; `%s_net.packetsTx.summation=>sent mul=-1` |
231 +| VM | property/perf charted when VM is discovered | `%s_net_drops` | `vsphere.vm_net_drops` | `vms net` | `drops` | `line` | `prioVmNetworkDrops` | `%s_net.droppedRx.summation=>received`; `%s_net.droppedTx.summation=>sent mul=-1` |
232 +| VM | property/perf charted when VM is discovered | `%s_overall_status` | `vsphere.vm_overall_status` | `vms status` | `status` | `line` | `prioVmOverallStatus` | `%s_overall.status.green=>green`; `%s_overall.status.red=>red`; `%s_overall.status.yellow=>yellow`; `%s_overall.status.gray=>gray` |
233 +| VM | property charted when VM is discovered | `%s_power_state` | `vsphere.vm_power_state` | `vms status` | `status` | `line` | `prioVMPowerState` | `%s_power_state.poweredOn=>powered_on`; `%s_power_state.poweredOff=>powered_off`; `%s_power_state.suspended=>suspended` |
234 +| VM | property charted when VM is discovered | `%s_connection_state` | `vsphere.vm_connection_state` | `vms status` | `status` | `line` | `prioVMConnectionState` | `%s_connection_state.connected=>connected`; `%s_connection_state.disconnected=>disconnected`; `%s_connection_state.orphaned=>orphaned`; `%s_connection_state.inaccessible=>inaccessible`; `%s_connection_state.invalid=>invalid` |
235 +| VM | property charted when VM is discovered | `%s_tools_running_status` | `vsphere.vm_tools_running_status` | `vms status` | `status` | `line` | `prioVMToolsRunningStatus` | `%s_tools_running_status.running=>running`; `%s_tools_running_status.notRunning=>not_running`; `%s_tools_running_status.executingScripts=>executing_scripts`; `%s_tools_running_status.unknown=>unknown` |
236 +| VM | property charted when VM is discovered | `%s_tools_version_status` | `vsphere.vm_tools_version_status` | `vms status` | `status` | `line` | `prioVMToolsVersionStatus` | `%s_tools_version_status.current=>current`; `%s_tools_version_status.needUpgrade=>need_upgrade`; `%s_tools_version_status.notInstalled=>not_installed`; `%s_tools_version_status.unmanaged=>unmanaged`; `%s_tools_version_status.tooOld=>too_old`; `%s_tools_version_status.supportedOld=>supported_old`; `%s_tools_version_status.supportedNew=>supported_new`; `%s_tools_version_status.tooNew=>too_new`; `%s_tools_version_status.blacklisted=>blacklisted`; `%s_tools_version_status.unknown=>unknown` |
237 +| VM | property charted when VM is discovered | `%s_consolidation_needed` | `vsphere.vm_consolidation_needed` | `vms status` | `status` | `line` | `prioVMConsolidationNeeded` | `%s_consolidation_needed.needed=>needed`; `%s_consolidation_needed.notNeeded=>not_needed` |
238 +| VM | property/perf charted when VM is discovered | `%s_system_uptime` | `vsphere.vm_system_uptime` | `vms uptime` | `seconds` | `line` | `prioVmSystemUptime` | `%s_sys.uptime.latest=>uptime` |
239 +| VM | property charted when VM is discovered | `%s_config_cpu` | `vsphere.vm_config_cpu` | `vms config` | `vCPUs` | `line` | `prioVMConfigCPU` | `%s_config_cpu=>vcpus` |
240 +| VM | property charted when VM is discovered | `%s_config_memory` | `vsphere.vm_config_memory` | `vms config` | `MiB` | `line` | `prioVMConfigMemory` | `%s_config_memory=>memory` |
241 +| VM | property charted when VM is discovered | `%s_config_devices` | `vsphere.vm_config_devices` | `vms config` | `devices` | `line` | `prioVMConfigDevices` | `%s_config_devices.disks=>disks`; `%s_config_devices.nics=>nics` |
242 +| VM | property charted when VM is discovered | `%s_storage_usage` | `vsphere.vm_storage_usage` | `vms storage` | `bytes` | `line` | `prioVMStorageUsage` | `%s_storage.committed=>committed`; `%s_storage.uncommitted=>uncommitted`; `%s_storage.unshared=>unshared` |
243 +| VM | snapshot property charted when VM is discovered | `%s_snapshot_count` | `vsphere.vm_snapshot_count` | `vms snapshots` | `snapshots` | `line` | `prioVMSnapshotCount` | `%s_snapshot_count=>count` |
244 +| VM | snapshot property charted when VM is discovered | `%s_snapshot_max_age` | `vsphere.vm_snapshot_max_age` | `vms snapshots` | `seconds` | `line` | `prioVMSnapshotAge` | `%s_snapshot_max_age=>age` |
245 +| VM | snapshot property charted when VM is discovered | `%s_snapshot_max_chain_depth` | `vsphere.vm_snapshot_max_chain_depth` | `vms snapshots` | `snapshots` | `line` | `prioVMSnapshotChainDepth` | `%s_snapshot_max_chain_depth=>depth` |
246 +| Host | property/perf charted when host is discovered | `%s_cpu_usage_total` | `vsphere.host_cpu_utilization` | `hosts cpu` | `percentage` | `line` | `prioHostCPUUtilization` | `%s_cpu.usage.average=>used div=100` |
247 +| Host | property/perf charted when host is discovered | `%s_mem_utilization` | `vsphere.host_mem_utilization` | `hosts mem` | `percentage` | `line` | `prioHostMemoryUtilization` | `%s_mem.usage.average=>used div=100` |
248 +| Host | property/perf charted when host is discovered | `%s_mem_usage` | `vsphere.host_mem_usage` | `hosts mem` | `KiB` | `line` | `prioHostMemoryUsage` | `%s_mem.granted.average=>granted`; `%s_mem.consumed.average=>consumed`; `%s_mem.active.average=>active`; `%s_mem.shared.average=>shared`; `%s_mem.sharedcommon.average=>sharedcommon` |
249 +| Host | property/perf charted when host is discovered | `%s_mem_swap_rate` | `vsphere.host_mem_swap_io` | `hosts mem` | `KiB/s` | `area` | `prioHostMemorySwapIO` | `%s_mem.swapinRate.average=>in`; `%s_mem.swapoutRate.average=>out` |
250 +| Host | property/perf charted when host is discovered | `%s_disk_io` | `vsphere.host_disk_io` | `hosts disk` | `KiB/s` | `area` | `prioHostDiskIO` | `%s_disk.read.average=>read`; `%s_disk.write.average=>write mul=-1` |
251 +| Host | property/perf charted when host is discovered | `%s_disk_max_latency` | `vsphere.host_disk_max_latency` | `hosts disk` | `milliseconds` | `line` | `prioHostDiskMaxLatency` | `%s_disk.maxTotalLatency.latest=>latency` |
252 +| Host | property/perf charted when host is discovered | `%s_net_traffic` | `vsphere.host_net_traffic` | `hosts net` | `KiB/s` | `area` | `prioHostNetworkTraffic` | `%s_net.bytesRx.average=>received`; `%s_net.bytesTx.average=>sent mul=-1` |
253 +| Host | property/perf charted when host is discovered | `%s_net_packets` | `vsphere.host_net_packets` | `hosts net` | `packets` | `line` | `prioHostNetworkPackets` | `%s_net.packetsRx.summation=>received`; `%s_net.packetsTx.summation=>sent mul=-1` |
254 +| Host | property/perf charted when host is discovered | `%s_net_drops_total` | `vsphere.host_net_drops` | `hosts net` | `drops` | `line` | `prioHostNetworkDrops` | `%s_net.droppedRx.summation=>received`; `%s_net.droppedTx.summation=>sent mul=-1` |
255 +| Host | property/perf charted when host is discovered | `%s_net_errors` | `vsphere.host_net_errors` | `hosts net` | `errors` | `line` | `prioHostNetworkErrors` | `%s_net.errorsRx.summation=>received`; `%s_net.errorsTx.summation=>sent mul=-1` |
256 +| Host | property/perf charted when host is discovered | `%s_overall_status` | `vsphere.host_overall_status` | `hosts status` | `status` | `line` | `prioHostOverallStatus` | `%s_overall.status.green=>green`; `%s_overall.status.red=>red`; `%s_overall.status.yellow=>yellow`; `%s_overall.status.gray=>gray` |
257 +| Host | property charted when host is discovered | `%s_power_state` | `vsphere.host_power_state` | `hosts status` | `status` | `line` | `prioHostPowerState` | `%s_power_state.poweredOn=>powered_on`; `%s_power_state.poweredOff=>powered_off`; `%s_power_state.standBy=>standby`; `%s_power_state.unknown=>unknown` |
258 +| Host | property charted when host is discovered | `%s_connection_state` | `vsphere.host_connection_state` | `hosts status` | `status` | `line` | `prioHostConnectionState` | `%s_connection_state.connected=>connected`; `%s_connection_state.notResponding=>not_responding`; `%s_connection_state.disconnected=>disconnected` |
259 +| Host | property charted when host is discovered | `%s_maintenance_status` | `vsphere.host_maintenance_status` | `hosts status` | `status` | `line` | `prioHostMaintenanceStatus` | `%s_maintenance_status.normal=>normal`; `%s_maintenance_status.inMaintenance=>in_maintenance` |
260 +| Host | property/perf charted when host is discovered | `%s_system_uptime` | `vsphere.host_system_uptime` | `hosts uptime` | `seconds` | `line` | `prioHostSystemUptime` | `%s_sys.uptime.latest=>uptime` |
261 +| Datastore | property charted when datastore is discovered; perf charts later only after perf data arrives | `%s_space_utilization` | `vsphere.datastore_space_utilization` | `datastores space` | `percentage` | `line` | `prioDatastoreSpaceUtilization` | `%s_used_space_pct=>used div=100` |
262 +| Datastore | property charted when datastore is discovered; perf charts later only after perf data arrives | `%s_space_usage` | `vsphere.datastore_space_usage` | `datastores space` | `bytes` | `line` | `prioDatastoreSpaceUsage` | `%s_capacity=>capacity`; `%s_free_space=>free`; `%s_used_space=>used`; `%s_uncommitted=>uncommitted` |
263 +| Datastore | property charted when datastore is discovered; perf charts later only after perf data arrives | `%s_overall_status` | `vsphere.datastore_overall_status` | `datastores status` | `status` | `line` | `prioDatastoreOverallStatus` | `%s_overall.status.green=>green`; `%s_overall.status.red=>red`; `%s_overall.status.yellow=>yellow`; `%s_overall.status.gray=>gray` |
264 +| Datastore | perf chart created only after datastore perf data arrives | `%s_disk_io` | `vsphere.datastore_disk_io` | `datastores disk` | `KiB/s` | `area` | `prioDatastoreDiskIO` | `%s_datastore.read.average=>read`; `%s_datastore.write.average=>write mul=-1` |
265 +| Datastore | perf chart created only after datastore perf data arrives | `%s_disk_iops` | `vsphere.datastore_disk_iops` | `datastores disk` | `operations/s` | `line` | `prioDatastoreDiskIOPS` | `%s_datastore.numberReadAveraged.average=>reads`; `%s_datastore.numberWriteAveraged.average=>writes mul=-1` |
266 +| Datastore | perf chart created only after datastore perf data arrives | `%s_disk_latency` | `vsphere.datastore_disk_latency` | `datastores disk` | `milliseconds` | `line` | `prioDatastoreDiskLatency` | `%s_datastore.totalReadLatency.average=>read`; `%s_datastore.totalWriteLatency.average=>write` |
267 +| Cluster | property chart created when cluster properties refresh | `%s_hosts` | `vsphere.cluster_hosts` | `clusters hosts` | `hosts` | `line` | `prioClusterHosts` | `%s_num_hosts=>total`; `%s_num_effective_hosts=>effective` |
268 +| Cluster | property chart created when cluster properties refresh | `%s_cpu_capacity` | `vsphere.cluster_cpu_capacity` | `clusters cpu` | `MHz` | `line` | `prioClusterCPUCapacity` | `%s_total_cpu=>total`; `%s_effective_cpu=>effective` |
269 +| Cluster | property chart created when cluster properties refresh | `%s_mem_capacity` | `vsphere.cluster_mem_capacity` | `clusters mem` | `bytes` | `line` | `prioClusterMemCapacity` | `%s_total_memory=>total`; `%s_effective_memory=>effective` |
270 +| Cluster | property chart created when cluster properties refresh | `%s_cpu_topology` | `vsphere.cluster_cpu_topology` | `clusters cpu` | `count` | `line` | `prioClusterCPUTopology` | `%s_num_cpu_cores=>cores`; `%s_num_cpu_threads=>threads` |
271 +| Cluster | property chart created when cluster properties refresh | `%s_drs_config` | `vsphere.cluster_drs_config` | `clusters config` | `status` | `line` | `prioClusterDRSConfig` | `%s_drs_enabled=>enabled` |
272 +| Cluster | property chart created when cluster properties refresh | `%s_drs_mode` | `vsphere.cluster_drs_mode` | `clusters config` | `status` | `line` | `prioClusterDRSMode` | `%s_drs_mode.manual=>manual`; `%s_drs_mode.partiallyAutomated=>partially_automated`; `%s_drs_mode.fullyAutomated=>fully_automated`; `%s_drs_mode.unknown=>unknown` |
273 +| Cluster | property chart created when cluster properties refresh | `%s_drs_vmotion_rate` | `vsphere.cluster_drs_vmotion_rate` | `clusters config` | `level` | `line` | `prioClusterDRSVmotionRate` | `%s_drs_vmotion_rate=>rate` |
274 +| Cluster | property chart created when cluster properties refresh | `%s_ha_config` | `vsphere.cluster_ha_config` | `clusters config` | `status` | `line` | `prioClusterHAConfig` | `%s_ha_enabled=>enabled`; `%s_ha_adm_ctrl_enabled=>admission_control` |
275 +| Cluster | property chart created when cluster properties refresh | `%s_ha_host_monitoring` | `vsphere.cluster_ha_host_monitoring` | `clusters config` | `status` | `line` | `prioClusterHAHostMonitoring` | `%s_ha_host_monitoring.enabled=>enabled`; `%s_ha_host_monitoring.disabled=>disabled`; `%s_ha_host_monitoring.unknown=>unknown` |
276 +| Cluster | property chart created when cluster properties refresh | `%s_ha_vm_monitoring` | `vsphere.cluster_ha_vm_monitoring` | `clusters config` | `status` | `line` | `prioClusterHAVMMonitoring` | `%s_ha_vm_monitoring.vmMonitoringDisabled=>disabled`; `%s_ha_vm_monitoring.vmMonitoringOnly=>vm_monitoring_only`; `%s_ha_vm_monitoring.vmAndAppMonitoring=>vm_and_app_monitoring`; `%s_ha_vm_monitoring.unknown=>unknown` |
277 +| Cluster | property chart created when cluster properties refresh | `%s_ha_vm_component_protection` | `vsphere.cluster_ha_vm_component_protection` | `clusters config` | `status` | `line` | `prioClusterHAVMComponentProtection` | `%s_ha_vm_component_protection.enabled=>enabled`; `%s_ha_vm_component_protection.disabled=>disabled`; `%s_ha_vm_component_protection.unknown=>unknown` |
278 +| Cluster | property chart created when cluster properties refresh | `%s_overall_status` | `vsphere.cluster_overall_status` | `clusters status` | `status` | `line` | `prioClusterOverallStatus` | `%s_overall.status.green=>green`; `%s_overall.status.red=>red`; `%s_overall.status.yellow=>yellow`; `%s_overall.status.gray=>gray` |
279 +| Cluster | property chart created when cluster properties refresh | `%s_vmotions` | `vsphere.cluster_vmotions` | `clusters migrations` | `migrations` | `line` | `prioClusterVMotions` | `%s_num_vmotions=>vmotions algo=incremental` |
280 +| Cluster | property chart created when cluster properties refresh | `%s_drs_score` | `vsphere.cluster_drs_score` | `clusters drs` | `percentage` | `line` | `prioClusterDRSScore` | `%s_drs_score=>score` |
281 +| Cluster | property chart created when cluster properties refresh | `%s_drs_balance` | `vsphere.cluster_drs_balance` | `clusters drs` | `score` | `line` | `prioClusterDRSBalance` | `%s_current_balance=>current div=1000`; `%s_target_balance=>target div=1000` |
282 +| Cluster | property chart created when cluster properties refresh | `%s_vm_count` | `vsphere.cluster_vm_count` | `clusters vms` | `VMs` | `line` | `prioClusterVMCount` | `%s_usage_total_vm_count=>total`; `%s_usage_powered_off_vm_count=>powered_off` |
283 +| Cluster | property chart created when cluster properties refresh | `%s_usage_cpu` | `vsphere.cluster_usage_cpu` | `clusters cpu` | `MHz` | `line` | `prioClusterUsageCPU` | `%s_usage_cpu_demand_mhz=>demand`; `%s_usage_cpu_entitled_mhz=>entitled`; `%s_usage_cpu_reservation_mhz=>reserved` |
284 +| Cluster | property chart created when cluster properties refresh | `%s_usage_mem` | `vsphere.cluster_usage_mem` | `clusters mem` | `MB` | `line` | `prioClusterUsageMem` | `%s_usage_mem_demand_mb=>demand`; `%s_usage_mem_entitled_mb=>entitled`; `%s_usage_mem_reservation_mb=>reserved` |
285 +| Cluster | perf chart created only after cluster perf data arrives | `%s_cpu_utilization` | `vsphere.cluster_cpu_utilization` | `clusters cpu` | `percentage` | `line` | `prioClusterCPUUtilization` | `%s_cpu.usage.average=>used div=100` |
286 +| Cluster | perf chart created only after cluster perf data arrives | `%s_cpu_usage_mhz` | `vsphere.cluster_cpu_usage` | `clusters cpu` | `MHz` | `line` | `prioClusterCPUUsage` | `%s_cpu.usagemhz.average=>used`; `%s_cpu.totalmhz.average=>total` |
287 +| Cluster | perf chart created only after cluster perf data arrives | `%s_mem_utilization` | `vsphere.cluster_mem_utilization` | `clusters mem` | `percentage` | `line` | `prioClusterMemUtilization` | `%s_mem.usage.average=>used div=100` |
288 +| Cluster | perf chart created only after cluster perf data arrives | `%s_mem_usage` | `vsphere.cluster_mem_usage` | `clusters mem` | `KiB` | `line` | `prioClusterMemUsage` | `%s_mem.consumed.average=>consumed`; `%s_mem.active.average=>active`; `%s_mem.granted.average=>granted`; `%s_mem.shared.average=>shared`; `%s_mem.overhead.average=>overhead`; `%s_mem.swapused.average=>swap_used` |
289 +| Cluster | perf chart created only after cluster perf data arrives | `%s_services_fairness` | `vsphere.cluster_services_fairness` | `clusters drs` | `score` | `line` | `prioClusterServicesFairness` | `%s_clusterServices.cpufairness.latest=>cpu`; `%s_clusterServices.memfairness.latest=>memory` |
290 +| Cluster | perf chart created only after cluster perf data arrives | `%s_services_effective_cpu` | `vsphere.cluster_services_effective_cpu` | `clusters cpu` | `MHz` | `line` | `prioClusterServicesEffectiveCPU` | `%s_clusterServices.effectivecpu.average=>effective_cpu` |
291 +| Cluster | perf chart created only after cluster perf data arrives | `%s_services_effective_mem` | `vsphere.cluster_services_effective_mem` | `clusters mem` | `MB` | `line` | `prioClusterServicesEffectiveMem` | `%s_clusterServices.effectivemem.average=>effective_mem` |
292 +| Cluster | perf chart created only after cluster perf data arrives | `%s_services_failover` | `vsphere.cluster_services_failover` | `clusters ha` | `failures` | `line` | `prioClusterServicesFailover` | `%s_clusterServices.failover.latest=>failures_tolerable` |
293 +| Cluster | perf chart created only after cluster perf data arrives | `%s_vm_migrations` | `vsphere.cluster_vm_migrations` | `clusters vmop` | `operations` | `line` | `prioClusterVMMigrations` | `%s_vmop.numVMotion.latest=>vmotion`; `%s_vmop.numSVMotion.latest=>svmotion`; `%s_vmop.numXVMotion.latest=>xvmotion` |
294 +| Cluster | perf chart created only after cluster perf data arrives | `%s_vm_lifecycle` | `vsphere.cluster_vm_lifecycle` | `clusters vmop` | `operations` | `line` | `prioClusterVMLifecycle` | `%s_vmop.numPoweron.latest=>poweron`; `%s_vmop.numPoweroff.latest=>poweroff`; `%s_vmop.numCreate.latest=>create`; `%s_vmop.numDestroy.latest=>destroy`; `%s_vmop.numClone.latest=>clone`; `%s_vmop.numDeploy.latest=>deploy` |
295 +| Cluster | perf chart created only after cluster perf data arrives | `%s_vm_management` | `vsphere.cluster_vm_management` | `clusters vmop` | `operations` | `line` | `prioClusterVMManagement` | `%s_vmop.numReconfigure.latest=>reconfigure`; `%s_vmop.numReset.latest=>reset`; `%s_vmop.numSuspend.latest=>suspend`; `%s_vmop.numRegister.latest=>register`; `%s_vmop.numUnregister.latest=>unregister` |
296 +| Cluster | perf chart created only after cluster perf data arrives | `%s_vm_guest_ops` | `vsphere.cluster_vm_guest_ops` | `clusters vmop` | `operations` | `line` | `prioClusterVMGuestOps` | `%s_vmop.numRebootGuest.latest=>reboot`; `%s_vmop.numShutdownGuest.latest=>shutdown`; `%s_vmop.numStandbyGuest.latest=>standby` |
297 +| Cluster | perf chart created only after cluster perf data arrives | `%s_vm_cold_migrations` | `vsphere.cluster_vm_cold_migrations` | `clusters vmop` | `operations` | `line` | `prioClusterVMColdMigrations` | `%s_vmop.numChangeDS.latest=>change_ds`; `%s_vmop.numChangeHost.latest=>change_host`; `%s_vmop.numChangeHostDS.latest=>change_host_ds` |
298 +| Resource pool | property chart created when resource pool properties refresh | `%s_cpu_usage` | `vsphere.resource_pool_cpu_usage` | `resource pools cpu` | `MHz` | `line` | `prioResourcePoolCPUUsage` | `%s_cpu_usage=>usage`; `%s_cpu_demand=>demand` |
299 +| Resource pool | property chart created when resource pool properties refresh | `%s_cpu_entitlement` | `vsphere.resource_pool_cpu_entitlement` | `resource pools cpu` | `MHz` | `line` | `prioResourcePoolCPUEntitlement` | `%s_cpu_entitlement_distributed=>distributed` |
300 +| Resource pool | property chart created when resource pool properties refresh | `%s_cpu_allocation` | `vsphere.resource_pool_cpu_allocation` | `resource pools cpu` | `MHz` | `line` | `prioResourcePoolCPUAllocation` | `%s_cpu_reservation_used=>reservation_used`; `%s_cpu_unreserved_for_vm=>unreserved_for_vm`; `%s_cpu_max_usage=>max_usage` |
301 +| Resource pool | property chart created when resource pool properties refresh | `%s_mem_usage` | `vsphere.resource_pool_mem_usage` | `resource pools mem` | `MB` | `line` | `prioResourcePoolMemUsage` | `%s_mem_usage_host=>host`; `%s_mem_usage_guest=>guest` |
302 +| Resource pool | property chart created when resource pool properties refresh | `%s_mem_entitlement` | `vsphere.resource_pool_mem_entitlement` | `resource pools mem` | `MB` | `line` | `prioResourcePoolMemEntitlement` | `%s_mem_entitlement_distributed=>distributed` |
303 +| Resource pool | property chart created when resource pool properties refresh | `%s_mem_allocation` | `vsphere.resource_pool_mem_allocation` | `resource pools mem` | `bytes` | `line` | `prioResourcePoolMemAllocation` | `%s_mem_reservation_used=>reservation_used`; `%s_mem_unreserved_for_vm=>unreserved_for_vm`; `%s_mem_max_usage=>max_usage` |
304 +| Resource pool | property chart created when resource pool properties refresh | `%s_mem_breakdown` | `vsphere.resource_pool_mem_breakdown` | `resource pools mem` | `MB` | `line` | `prioResourcePoolMemBreakdown` | `%s_mem_private=>private`; `%s_mem_shared=>shared`; `%s_mem_swapped=>swapped`; `%s_mem_ballooned=>ballooned`; `%s_mem_overhead=>overhead`; `%s_mem_consumed_overhead=>consumed_overhead`; `%s_mem_compressed=>compressed div=1024` |
305 +| Resource pool | property chart created when resource pool properties refresh | `%s_cpu_config` | `vsphere.resource_pool_cpu_config` | `resource pools cpu` | `MHz` | `line` | `prioResourcePoolCPUConfig` | `%s_cpu_reservation=>reservation`; `%s_cpu_limit=>limit` |
306 +| Resource pool | property chart created when resource pool properties refresh | `%s_mem_config` | `vsphere.resource_pool_mem_config` | `resource pools mem` | `MB` | `line` | `prioResourcePoolMemConfig` | `%s_mem_reservation=>reservation`; `%s_mem_limit=>limit` |
307 +| Resource pool | property chart created when resource pool properties refresh | `%s_overall_status` | `vsphere.resource_pool_overall_status` | `resource pools status` | `status` | `line` | `prioResourcePoolOverallStatus` | `%s_overall.status.green=>green`; `%s_overall.status.red=>red`; `%s_overall.status.yellow=>yellow`; `%s_overall.status.gray=>gray` |
308 +
309 +## Metric Source Lists
310 +
311 +Performance counter lists are selected in
312 +`src/go/plugin/go.d/collector/vsphere/discover/metric_lists.go`.
313 +
314 +VM performance counters:
315 +
316 +- `cpu.usage.average`
317 +- `mem.usage.average`
318 +- `mem.granted.average`
319 +- `mem.consumed.average`
320 +- `mem.active.average`
321 +- `mem.shared.average`
322 +- `mem.swapinRate.average`
323 +- `mem.swapoutRate.average`
324 +- `mem.swapped.average`
325 +- `net.bytesRx.average`
326 +- `net.bytesTx.average`
327 +- `net.packetsRx.summation`
328 +- `net.packetsTx.summation`
329 +- `net.droppedRx.summation`
330 +- `net.droppedTx.summation`
331 +- `disk.read.average`
332 +- `disk.write.average`
333 +- `disk.maxTotalLatency.latest`
334 +- `sys.uptime.latest`
335 +
336 +Host performance counters:
337 +
338 +- `cpu.usage.average`
339 +- `mem.usage.average`
340 +- `mem.granted.average`
341 +- `mem.consumed.average`
342 +- `mem.active.average`
343 +- `mem.shared.average`
344 +- `mem.sharedcommon.average`
345 +- `mem.swapinRate.average`
346 +- `mem.swapoutRate.average`
347 +- `net.bytesRx.average`
348 +- `net.bytesTx.average`
349 +- `net.packetsRx.summation`
350 +- `net.packetsTx.summation`
351 +- `net.droppedRx.summation`
352 +- `net.droppedTx.summation`
353 +- `net.errorsRx.summation`
354 +- `net.errorsTx.summation`
355 +- `disk.read.average`
356 +- `disk.write.average`
357 +- `disk.maxTotalLatency.latest`
358 +- `sys.uptime.latest`
359 +
360 +Power counters requested with empty instance when vSphere exposes them:
361 +
362 +- VM: `power.power.average`
363 +- VM: `power.energy.summation`
364 +- Host: `power.power.average`
365 +- Host: `power.powerCap.average`
366 +- Host: `power.energy.summation`
367 +- Host: `power.capacity.usable.average`
368 +- Host: `power.capacity.usage.average`
369 +- Host: `power.capacity.usagePct.average`
370 +- Host: `power.capacity.usageIdle.average`
371 +- Host: `power.capacity.usageSystem.average`
372 +- Host: `power.capacity.usageVm.average`
373 +
374 +Datastore performance counters:
375 +
376 +- `datastore.numberReadAveraged.average`
377 +- `datastore.numberWriteAveraged.average`
378 +- `datastore.totalReadLatency.average`
379 +- `datastore.totalWriteLatency.average`
380 +- `datastore.read.average`
381 +- `datastore.write.average`
382 +
383 +Cluster performance counters:
384 +
385 +- `clusterServices.effectivecpu.average`
386 +- `clusterServices.effectivemem.average`
387 +- `clusterServices.cpufairness.latest`
388 +- `clusterServices.memfairness.latest`
389 +- `clusterServices.failover.latest`
390 +- `cpu.usage.average`
391 +- `cpu.usagemhz.average`
392 +- `cpu.totalmhz.average`
393 +- `mem.usage.average`
394 +- `mem.consumed.average`
395 +- `mem.overhead.average`
396 +- `mem.active.average`
397 +- `mem.granted.average`
398 +- `mem.shared.average`
399 +- `mem.swapused.average`
400 +- `vmop.numVMotion.latest`
401 +- `vmop.numSVMotion.latest`
402 +- `vmop.numXVMotion.latest`
403 +- `vmop.numPoweron.latest`
404 +- `vmop.numPoweroff.latest`
405 +- `vmop.numCreate.latest`
406 +- `vmop.numDestroy.latest`
407 +- `vmop.numClone.latest`
408 +- `vmop.numDeploy.latest`
409 +- `vmop.numReset.latest`
410 +- `vmop.numSuspend.latest`
411 +- `vmop.numReconfigure.latest`
412 +- `vmop.numRegister.latest`
413 +- `vmop.numUnregister.latest`
414 +- `vmop.numChangeDS.latest`
415 +- `vmop.numChangeHost.latest`
416 +- `vmop.numChangeHostDS.latest`
417 +- `vmop.numRebootGuest.latest`
418 +- `vmop.numShutdownGuest.latest`
419 +- `vmop.numStandbyGuest.latest`
420 +- `clusterServices.clusterDrsScore.latest`
421 +- `clusterServices.vmDrsScore.latest`
422 +
423 +## Property Metric Semantics
424 +
425 +| Resource | Property path requested | Metric behavior |
426 +|---|---|---|
427 +| VM | discovery requests `name`, `parent`, `runtime.host`, `runtime.connectionState`, `runtime.powerState`, `runtime.consolidationNeeded`, `summary.guest`, `summary.config`, `summary.storage`, `summary.overallStatus`, `snapshot`; also `config.instanceUuid` only when `collect_vsan` is enabled | Emits `overall.status.{green,red,yellow,gray}`, `power_state.*`, `connection_state.*`, VMware Tools running/version state, disk consolidation-needed state, configured CPU/memory/device counts, aggregate storage usage, and snapshot aggregate metrics for all discovered VMs. Emits real-time aggregate performance counters only when the VM is `poweredOn`. If `runtime.host` is absent for a non-running VM, the VM folder parent is used to recover the datacenter label when possible. Guest hostname/IP and guest OS labels are excluded from this PR by user decision. Per-virtual-disk capacity/performance and per-vNIC performance are excluded from this PR by 2026-05-22 user decision. VM power and energy metrics are emitted when vSphere returns aggregate `power.*` counters. VM vSAN performance is emitted only when `collect_vsan` is enabled and vSAN returns a matching VM instance UUID. |
428 +| Host | discovery requests `name`, `parent`, `runtime.connectionState`, `runtime.powerState`, `runtime.inMaintenanceMode`, `summary.overallStatus`; also `config.vsanHostConfig.clusterInfo.nodeUuid` only when `collect_vsan` is enabled | Emits `overall.status.{green,red,yellow,gray}`, `power_state.*`, `connection_state.*`, and `maintenance_status.*` for all discovered hosts. Emits real-time aggregate performance counters only when the host is `poweredOn`. Per-host child-instance NIC, disk, storage-adapter, storage-path, and CPU-instance performance metrics are excluded from this PR by 2026-05-22 user decision. Host power, energy, and power-capacity metrics are emitted when vSphere returns aggregate `power.*` counters. Host vSAN performance is emitted only when `collect_vsan` is enabled and vSAN returns a matching host node UUID. |
429 +| Datastore | refresh requests `summary`, `overallStatus` | Emits `capacity`, `free_space`, `used_space`, `used_space_pct`, and `overall.status.*`. Capacity/free/used are zeroed when `Accessible=false`. |
430 +| Network | discovery requests `name`, `parent`, `summary`, `host`, and `vm` only when `collect_network_topology` is enabled | Emits no metrics. Cached Network and Distributed Virtual Port Group status/relationships are used only by the topology Function. |
431 +| Datastore cluster | discovery requests `StoragePod` `name`, `parent`, `summary`, `podStorageDrsEntry`; only when `collect_datastore_clusters` is enabled | Emits optional StoragePod capacity, free, used, utilization, and Storage DRS enabled/disabled status for datastore clusters matching `datastore_cluster_include`. |
432 +| Cluster | refresh requests `name`, `summary`, `configurationEx`, `overallStatus`; discovery includes `configurationEx.vsanConfigInfo` only when `collect_vsan` is enabled | Emits capacity/topology/DRS/HA/usage/overall-status property metrics, with conditional fields reset to zero before update. vSAN cluster space, health, and performance metrics are emitted only when `collect_vsan` is enabled, the cluster is vSAN-enabled, and vSAN API calls return data. |
433 +| Resource pool | refresh requests `name`, `summary`, `config`, `runtime`, `overallStatus` | Emits quick stats, runtime allocation, config reservation/limit, memory breakdown, and overall-status metrics. |
434 +
435 +## Lifecycle Contract
436 +
437 +- Hosts, VMs, datastores, clusters, resource pools, and optional datastore
438 + clusters are tracked in discovered maps.
439 +- Each absent resource increments a failure counter per collection.
440 +- `failedUpdatesLimit` is `10`.
441 +- When the failure counter reaches `10`, charts whose ID starts with
442 + `<resourceID>_` are marked removed and not-created, making them obsolete.
443 +- Datastore property charts are created when the datastore is present.
444 +- Datastore performance charts are created only after performance data arrives.
445 +- Cluster property charts are created when the cluster is present.
446 +- Cluster performance charts are created only after performance data arrives.
447 +- Non-powered-on hosts and VMs are discovered when vSphere returns them and the
448 + include selectors keep them. Their property/status metrics keep the resource
449 + alive; real-time host/VM performance query specs are not generated for them.
450 +- Optional VM virtual disk capacity/performance and VM network-interface
451 + performance metrics are not part of this PR. They were hard-removed before
452 + merge by 2026-05-22 user decision. Existing aggregate VM disk and network
453 + contexts remain default-on.
454 +- Optional datastore cluster metrics are not part of the legacy V1 surface.
455 + They are emitted only when `collect_datastore_clusters` is enabled, only for
456 + StoragePod objects matching `datastore_cluster_include`.
457 +- Optional host disk/LUN/device, storage-adapter, storage-path, and CPU-instance
458 + metrics are not part of this PR. They were hard-removed before merge by
459 + 2026-05-22 user decision. Existing aggregate host disk, network, CPU, memory,
460 + and uptime contexts remain default-on.
461 +- Host/VM power metrics are not part of the legacy V1 surface. They are
462 + requested by adding selected aggregate `power.*` counters with empty instance
463 + to powered-on host and VM performance queries when vSphere exposes those
464 + counters. They emit one aggregate set per included host or VM and therefore
465 + use the existing host/VM include selectors instead of a new child selector.
466 + The `collect_power_metrics` option was removed before merge by 2026-05-22
467 + user decision.
468 +- Optional vSAN metrics are not part of the legacy V1 surface. They are queried
469 + only when `collect_vsan` is enabled, only for clusters whose vSAN config is
470 + enabled and match the dedicated vSAN selectors, and only through the
471 + vSAN Management API. Missing vSAN API support, missing vSAN Performance
472 + Service, or unavailable vSAN counters cause one-time warnings and no emitted
473 + vSAN series for that query. The first implemented vSAN surface covers cluster
474 + space, cluster health, and cluster/host/VM vSAN performance; vSAN events and
475 + deeper disk-group, disk, component, or CMMDS entity metrics are not emitted by
476 + this option.
477 +
478 +## Health Alert Contract
479 +
480 +| Template | Context | Lookup/calc | Warning | Critical | Recipient |
481 +|---|---|---|---|---|---|
482 +| `vsphere_vm_cpu_utilization` | `vsphere.vm_cpu_utilization` | `average -10m unaligned match-names of used` | `$this > (($status >= $WARNING) ? (75) : (85))` | `$this > (($status == $CRITICAL) ? (85) : (95))` | `silent` |
483 +| `vsphere_vm_mem_utilization` | `vsphere.vm_mem_utilization` | `$used` | `$this > (($status >= $WARNING) ? (80) : (90))` | `$this > (($status == $CRITICAL) ? (90) : (98))` | `silent` |
484 +| `vsphere_vm_snapshot_chain_depth` | `vsphere.vm_snapshot_max_chain_depth` | `$depth` | `$this > 3` | none | `sysadmin` |
485 +| `vsphere_vm_snapshot_age` | `vsphere.vm_snapshot_max_age` | `$age` | none | `$this > 86400` | `sysadmin` |
486 +| `vsphere_host_cpu_utilization` | `vsphere.host_cpu_utilization` | `average -10m unaligned match-names of used` | `$this > (($status >= $WARNING) ? (75) : (85))` | `$this > (($status == $CRITICAL) ? (85) : (95))` | `sysadmin` |
487 +| `vsphere_host_mem_utilization` | `vsphere.host_mem_utilization` | `$used` | `$this > (($status >= $WARNING) ? (80) : (90))` | `$this > (($status == $CRITICAL) ? (90) : (98))` | `sysadmin` |
488 +
489 +## Current Artifact Drift To Preserve Or Fix Explicitly
490 +
491 +The migration must preserve runtime behavior from code. Existing artifact drift
492 +must be fixed only as explicit metadata/docs updates:
493 +
494 +- Code sets `vsphere.host_net_traffic` chart type to `area`; metadata currently
495 + says `line`.
496 +- Code sets VM and host network drop units to `drops`; metadata currently says
497 + `packets`.
498 +- Cluster performance counter selection includes
499 + `clusterServices.clusterDrsScore.latest` and
500 + `clusterServices.vmDrsScore.latest`, but the current chart templates do not
501 + expose chart dimensions for those exact counter keys. A v2 migration must not
502 + accidentally create public series for them unless an enrichment row explicitly
503 + adds new contexts/dimensions.
integrations/gen_docs_integrations.py
+1
@@ -87,6 +87,7 @@ def build_path(meta_yaml_link: str) -> str:
87 meta_yaml_link.replace("https://github.com/netdata/", "")
88 .split("/", 1)[1]
89 .replace("edit/master/", "")
90 + .replace("blob/master/", "")
91 .replace("/metadata.yaml", "")
92 )
93
src/go/pkg/matcher/simple_patterns.go
+41 -8
@@ -3,6 +3,8 @@
3 package matcher
4
5 import (
6 + "errors"
7 + "fmt"
8 "strings"
9 )
10
@@ -21,7 +23,12 @@ func NewSimplePatternsMatcher(expr string) (Matcher, error) {
23 ps := simplePatternsMatcher{}
24
25 for pattern := range strings.FieldsSeq(expr) {
24 - if err := ps.add(pattern); err != nil {
26 + positive := true
27 + if strings.HasPrefix(pattern, "!") {
28 + positive = false
29 + pattern = strings.TrimPrefix(pattern, "!")
30 + }
31 + if err := ps.add(pattern, positive); err != nil {
32 return nil, err
33 }
34 }
@@ -31,14 +38,40 @@ func NewSimplePatternsMatcher(expr string) (Matcher, error) {
38 return ps, nil
39 }
40
34 -func (m *simplePatternsMatcher) add(term string) error {
35 - p := simplePatternTerm{}
36 - if term[0] == '!' {
37 - p.positive = false
38 - term = term[1:]
39 - } else {
40 - p.positive = true
41 +// NewSimplePatternListMatcher creates a simple-patterns matcher from a pre-split
42 +// list of glob patterns. Use it when individual patterns may contain whitespace.
43 +func NewSimplePatternListMatcher(patterns []string) (Matcher, error) {
44 + ps := simplePatternsMatcher{}
45 + hasPositive := false
46 +
47 + for _, pattern := range patterns {
48 + pattern = strings.TrimSpace(pattern)
49 + if pattern == "" {
50 + continue
51 + }
52 + negative := strings.HasPrefix(pattern, "!")
53 + if negative {
54 + pattern = strings.TrimSpace(strings.TrimPrefix(pattern, "!"))
55 + }
56 + if pattern == "" {
57 + return nil, errors.New("invalid empty negative pattern")
58 + }
59 + hasPositive = hasPositive || !negative
60 + if err := ps.add(pattern, !negative); err != nil {
61 + return nil, fmt.Errorf("invalid pattern: %w", err)
62 + }
63 }
64 + if len(ps) == 0 {
65 + return FALSE(), nil
66 + }
67 + if !hasPositive {
68 + return nil, errors.New("must include at least one positive pattern")
69 + }
70 + return ps, nil
71 +}
72 +
73 +func (m *simplePatternsMatcher) add(term string, positive bool) error {
74 + p := simplePatternTerm{positive: positive}
75 matcher, err := NewGlobMatcher(term)
76 if err != nil {
77 return err
src/go/pkg/matcher/simple_patterns_test.go
+95
@@ -46,6 +46,101 @@ func TestNewSimplePatternsMatcher(t *testing.T) {
46 }
47 }
48
49 +func TestNewSimplePatternListMatcher(t *testing.T) {
50 + tests := map[string]struct {
51 + patterns []string
52 + want Matcher
53 + wantErr string
54 + }{
55 + "empty list returns false": {
56 + want: FALSE(),
57 + },
58 + "blank entries return false": {
59 + patterns: []string{"", " ", "\t"},
60 + want: FALSE(),
61 + },
62 + "single glob": {
63 + patterns: []string{"foo*"},
64 + want: simplePatternsMatcher{
65 + {stringPrefixMatcher("foo"), true},
66 + },
67 + },
68 + "preserves whitespace inside pattern": {
69 + patterns: []string{"Business Unit"},
70 + want: simplePatternsMatcher{
71 + {stringFullMatcher("Business Unit"), true},
72 + },
73 + },
74 + "bare negative marker is invalid": {
75 + patterns: []string{"!"},
76 + wantErr: "invalid empty negative pattern",
77 + },
78 + "blank negative pattern is invalid": {
79 + patterns: []string{"! "},
80 + wantErr: "invalid empty negative pattern",
81 + },
82 + "invalid glob is wrapped": {
83 + patterns: []string{"["},
84 + wantErr: "invalid pattern",
85 + },
86 + "all negative patterns are invalid": {
87 + patterns: []string{"!Business Secret"},
88 + wantErr: "must include at least one positive pattern",
89 + },
90 + }
91 +
92 + for name, test := range tests {
93 + t.Run(name, func(t *testing.T) {
94 + matcher, err := NewSimplePatternListMatcher(test.patterns)
95 + if test.wantErr != "" {
96 + require.ErrorContains(t, err, test.wantErr)
97 + return
98 + }
99 + require.NoError(t, err)
100 + assert.Equal(t, test.want, matcher)
101 + })
102 + }
103 +}
104 +
105 +func TestSimplePatternList_Match(t *testing.T) {
106 + tests := map[string]struct {
107 + patterns []string
108 + value string
109 + want bool
110 + }{
111 + "positive before negative wins": {
112 + patterns: []string{"Business*", "!Business Secret"},
113 + value: "Business Secret",
114 + want: true,
115 + },
116 + "negative before positive wins": {
117 + patterns: []string{"!Business Secret", "Business*"},
118 + value: "Business Secret",
119 + want: false,
120 + },
121 + "later positive matches": {
122 + patterns: []string{"!Business Secret", "Cost Center", "Business*"},
123 + value: "Cost Center",
124 + want: true,
125 + },
126 + "no matching pattern": {
127 + patterns: []string{"!Business Secret", "Cost Center", "Business*"},
128 + value: "Cost",
129 + want: false,
130 + },
131 + }
132 +
133 + for name, test := range tests {
134 + t.Run(name, func(t *testing.T) {
135 + m, err := NewSimplePatternListMatcher(test.patterns)
136 + require.NoError(t, err)
137 +
138 + assert.Equal(t, test.want, m.MatchString(test.value))
139 + assert.Equal(t, test.want, m.Match([]byte(test.value)))
140 + })
141 + }
142 +}
143 +
144 func TestSimplePatterns_Match(t *testing.T) {
145 m, err := NewSimplePatternsMatcher("*foobar* !foo* !*bar *")
146
src/go/plugin/go.d/collector/vsphere/chart_template_test.go new
+38
@@ -0,0 +1,38 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import (
6 + "testing"
7 +
8 + "github.com/stretchr/testify/require"
9 +
10 + "github.com/netdata/netdata/go/plugins/plugin/framework/chartengine"
11 + "github.com/netdata/netdata/go/plugins/plugin/framework/charttpl"
12 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
13 +)
14 +
15 +func TestCollector_ChartTemplateYAML(t *testing.T) {
16 + collecttest.AssertChartTemplateSchema(t, chartTemplateYAML)
17 +
18 + spec, err := charttpl.DecodeYAML([]byte(chartTemplateYAML))
19 + require.NoError(t, err)
20 + assertUniqueChartPriorities(t, spec)
21 +
22 + _, err = chartengine.Compile(spec, 1)
23 + require.NoError(t, err)
24 +}
25 +
26 +func assertUniqueChartPriorities(t *testing.T, spec *charttpl.Spec) {
27 + t.Helper()
28 +
29 + seen := make(map[int]string)
30 + for _, group := range spec.Groups {
31 + for _, chart := range group.Charts {
32 + if other, ok := seen[chart.Priority]; ok {
33 + require.Failf(t, "duplicate chart priority", "priority %d is used by %s and %s", chart.Priority, other, chart.Context)
34 + }
35 + seen[chart.Priority] = chart.Context
36 + }
37 + }
38 +}
src/go/plugin/go.d/collector/vsphere/charts.go deleted
-1300
@@ -1,1300 +0,0 @@
1 -// SPDX-License-Identifier: GPL-3.0-or-later
2 -
3 -package vsphere
4 -
5 -import (
6 - "fmt"
7 - "strings"
8 -
9 - "github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi"
10 - rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
11 -)
12 -
13 -const (
14 - prioClusterHosts = collectorapi.Priority + iota
15 - prioClusterCPUCapacity
16 - prioClusterMemCapacity
17 - prioClusterCPUTopology
18 - prioClusterDRSConfig
19 - prioClusterHAConfig
20 - prioClusterOverallStatus
21 - prioClusterVMotions
22 - prioClusterDRSScore
23 - prioClusterDRSBalance
24 - prioClusterVMCount
25 - prioClusterUsageCPU
26 - prioClusterUsageMem
27 - prioClusterCPUUtilization
28 - prioClusterCPUUsage
29 - prioClusterMemUtilization
30 - prioClusterMemUsage
31 - prioClusterServicesFairness
32 - prioClusterServicesEffectiveCPU
33 - prioClusterServicesEffectiveMem
34 - prioClusterServicesFailover
35 - prioClusterVMMigrations
36 - prioClusterVMLifecycle
37 - prioClusterVMManagement
38 - prioClusterVMGuestOps
39 - prioClusterVMColdMigrations
40 -
41 - prioResourcePoolCPUUsage
42 - prioResourcePoolCPUEntitlement
43 - prioResourcePoolCPUAllocation
44 - prioResourcePoolMemUsage
45 - prioResourcePoolMemEntitlement
46 - prioResourcePoolMemAllocation
47 - prioResourcePoolMemBreakdown
48 - prioResourcePoolCPUConfig
49 - prioResourcePoolMemConfig
50 - prioResourcePoolOverallStatus
51 -
52 - prioDatastoreDiskIO
53 - prioDatastoreDiskIOPS
54 - prioDatastoreDiskLatency
55 - prioDatastoreSpaceUtilization
56 - prioDatastoreSpaceUsage
57 - prioDatastoreOverallStatus
58 -
59 - prioVMCPUUtilization
60 - prioVmMemoryUtilization
61 - prioVmMemoryUsage
62 - prioVmMemorySwapUsage
63 - prioVmMemorySwapIO
64 - prioVmDiskIO
65 - prioVmDiskMaxLatency
66 - prioVmNetworkTraffic
67 - prioVmNetworkPackets
68 - prioVmNetworkDrops
69 - prioVmOverallStatus
70 - prioVmSystemUptime
71 -
72 - prioHostCPUUtilization
73 - prioHostMemoryUtilization
74 - prioHostMemoryUsage
75 - prioHostMemorySwapIO
76 - prioHostDiskIO
77 - prioHostDiskMaxLatency
78 - prioHostNetworkTraffic
79 - prioHostNetworkPackets
80 - prioHostNetworkDrops
81 - prioHostNetworkErrors
82 - prioHostOverallStatus
83 - prioHostSystemUptime
84 -)
85 -
86 -var (
87 - vmChartsTmpl = collectorapi.Charts{
88 - vmCPUUtilizationChartTmpl.Copy(),
89 -
90 - vmMemoryUtilizationChartTmpl.Copy(),
91 - vmMemoryUsageChartTmpl.Copy(),
92 - vmMemorySwapUsageChartTmpl.Copy(),
93 - vmMemorySwapIOChartTmpl.Copy(),
94 -
95 - vmDiskIOChartTmpl.Copy(),
96 - vmDiskMaxLatencyChartTmpl.Copy(),
97 -
98 - vmNetworkTrafficChartTmpl.Copy(),
99 - vmNetworkPacketsChartTmpl.Copy(),
100 - vmNetworkDropsChartTmpl.Copy(),
101 -
102 - vmOverallStatusChartTmpl.Copy(),
103 -
104 - vmSystemUptimeChartTmpl.Copy(),
105 - }
106 -
107 - vmCPUUtilizationChartTmpl = collectorapi.Chart{
108 - ID: "%s_cpu_utilization",
109 - Title: "Virtual Machine CPU utilization",
110 - Units: "percentage",
111 - Fam: "vms cpu",
112 - Ctx: "vsphere.vm_cpu_utilization",
113 - Priority: prioVMCPUUtilization,
114 - Dims: collectorapi.Dims{
115 - {ID: "%s_cpu.usage.average", Name: "used", Div: 100},
116 - },
117 - }
118 -
119 - // Ref: https://www.vmware.com/support/developer/converter-sdk/conv51_apireference/memory_counters.html
120 - vmMemoryUtilizationChartTmpl = collectorapi.Chart{
121 - ID: "%s_mem_utilization",
122 - Title: "Virtual Machine memory utilization",
123 - Units: "percentage",
124 - Fam: "vms mem",
125 - Ctx: "vsphere.vm_mem_utilization",
126 - Priority: prioVmMemoryUtilization,
127 - Dims: collectorapi.Dims{
128 - {ID: "%s_mem.usage.average", Name: "used", Div: 100},
129 - },
130 - }
131 - vmMemoryUsageChartTmpl = collectorapi.Chart{
132 - ID: "%s_mem_usage",
133 - Title: "Virtual Machine memory usage",
134 - Units: "KiB",
135 - Fam: "vms mem",
136 - Ctx: "vsphere.vm_mem_usage",
137 - Priority: prioVmMemoryUsage,
138 - Dims: collectorapi.Dims{
139 - {ID: "%s_mem.granted.average", Name: "granted"},
140 - {ID: "%s_mem.consumed.average", Name: "consumed"},
141 - {ID: "%s_mem.active.average", Name: "active"},
142 - {ID: "%s_mem.shared.average", Name: "shared"},
143 - },
144 - }
145 - vmMemorySwapUsageChartTmpl = collectorapi.Chart{
146 - ID: "%s_mem_swap_usage",
147 - Title: "Virtual Machine VMKernel memory swap usage",
148 - Units: "KiB",
149 - Fam: "vms mem",
150 - Ctx: "vsphere.vm_mem_swap_usage",
151 - Priority: prioVmMemorySwapUsage,
152 - Dims: collectorapi.Dims{
153 - {ID: "%s_mem.swapped.average", Name: "swapped"},
154 - },
155 - }
156 - vmMemorySwapIOChartTmpl = collectorapi.Chart{
157 - ID: "%s_mem_swap_io_rate",
158 - Title: "Virtual Machine VMKernel memory swap IO",
159 - Units: "KiB/s",
160 - Fam: "vms mem",
161 - Ctx: "vsphere.vm_mem_swap_io",
162 - Type: collectorapi.Area,
163 - Priority: prioVmMemorySwapIO,
164 - Dims: collectorapi.Dims{
165 - {ID: "%s_mem.swapinRate.average", Name: "in"},
166 - {ID: "%s_mem.swapoutRate.average", Name: "out"},
167 - },
168 - }
169 -
170 - vmDiskIOChartTmpl = collectorapi.Chart{
171 - ID: "%s_disk_io",
172 - Title: "Virtual Machine disk IO",
173 - Units: "KiB/s",
174 - Fam: "vms disk",
175 - Ctx: "vsphere.vm_disk_io",
176 - Type: collectorapi.Area,
177 - Priority: prioVmDiskIO,
178 - Dims: collectorapi.Dims{
179 - {ID: "%s_disk.read.average", Name: "read"},
180 - {ID: "%s_disk.write.average", Name: "write", Mul: -1},
181 - },
182 - }
183 - vmDiskMaxLatencyChartTmpl = collectorapi.Chart{
184 - ID: "%s_disk_max_latency",
185 - Title: "Virtual Machine disk max latency",
186 - Units: "milliseconds",
187 - Fam: "vms disk",
188 - Ctx: "vsphere.vm_disk_max_latency",
189 - Priority: prioVmDiskMaxLatency,
190 - Dims: collectorapi.Dims{
191 - {ID: "%s_disk.maxTotalLatency.latest", Name: "latency"},
192 - },
193 - }
194 -
195 - vmNetworkTrafficChartTmpl = collectorapi.Chart{
196 - ID: "%s_net_traffic",
197 - Title: "Virtual Machine network traffic",
198 - Units: "KiB/s",
199 - Fam: "vms net",
200 - Ctx: "vsphere.vm_net_traffic",
201 - Type: collectorapi.Area,
202 - Priority: prioVmNetworkTraffic,
203 - Dims: collectorapi.Dims{
204 - {ID: "%s_net.bytesRx.average", Name: "received"},
205 - {ID: "%s_net.bytesTx.average", Name: "sent", Mul: -1},
206 - },
207 - }
208 - vmNetworkPacketsChartTmpl = collectorapi.Chart{
209 - ID: "%s_net_packets",
210 - Title: "Virtual Machine network packets",
211 - Units: "packets",
212 - Fam: "vms net",
213 - Ctx: "vsphere.vm_net_packets",
214 - Priority: prioVmNetworkPackets,
215 - Dims: collectorapi.Dims{
216 - {ID: "%s_net.packetsRx.summation", Name: "received"},
217 - {ID: "%s_net.packetsTx.summation", Name: "sent", Mul: -1},
218 - },
219 - }
220 - vmNetworkDropsChartTmpl = collectorapi.Chart{
221 - ID: "%s_net_drops",
222 - Title: "Virtual Machine network dropped packets",
223 - Units: "drops",
224 - Fam: "vms net",
225 - Ctx: "vsphere.vm_net_drops",
226 - Priority: prioVmNetworkDrops,
227 - Dims: collectorapi.Dims{
228 - {ID: "%s_net.droppedRx.summation", Name: "received"},
229 - {ID: "%s_net.droppedTx.summation", Name: "sent", Mul: -1},
230 - },
231 - }
232 -
233 - vmOverallStatusChartTmpl = collectorapi.Chart{
234 - ID: "%s_overall_status",
235 - Title: "Virtual Machine overall alarm status",
236 - Units: "status",
237 - Fam: "vms status",
238 - Ctx: "vsphere.vm_overall_status",
239 - Priority: prioVmOverallStatus,
240 - Dims: collectorapi.Dims{
241 - {ID: "%s_overall.status.green", Name: "green"},
242 - {ID: "%s_overall.status.red", Name: "red"},
243 - {ID: "%s_overall.status.yellow", Name: "yellow"},
244 - {ID: "%s_overall.status.gray", Name: "gray"},
245 - },
246 - }
247 -
248 - vmSystemUptimeChartTmpl = collectorapi.Chart{
249 - ID: "%s_system_uptime",
250 - Title: "Virtual Machine system uptime",
251 - Units: "seconds",
252 - Fam: "vms uptime",
253 - Ctx: "vsphere.vm_system_uptime",
254 - Priority: prioVmSystemUptime,
255 - Dims: collectorapi.Dims{
256 - {ID: "%s_sys.uptime.latest", Name: "uptime"},
257 - },
258 - }
259 -)
260 -
261 -var (
262 - hostChartsTmpl = collectorapi.Charts{
263 - hostCPUUtilizationChartTmpl.Copy(),
264 -
265 - hostMemUtilizationChartTmpl.Copy(),
266 - hostMemUsageChartTmpl.Copy(),
267 - hostMemSwapIOChartTmpl.Copy(),
268 -
269 - hostDiskIOChartTmpl.Copy(),
270 - hostDiskMaxLatencyChartTmpl.Copy(),
271 -
272 - hostNetworkTraffic.Copy(),
273 - hostNetworkPacketsChartTmpl.Copy(),
274 - hostNetworkDropsChartTmpl.Copy(),
275 - hostNetworkErrorsChartTmpl.Copy(),
276 -
277 - hostOverallStatusChartTmpl.Copy(),
278 -
279 - hostSystemUptimeChartTmpl.Copy(),
280 - }
281 - hostCPUUtilizationChartTmpl = collectorapi.Chart{
282 - ID: "%s_cpu_usage_total",
283 - Title: "ESXi Host CPU utilization",
284 - Units: "percentage",
285 - Fam: "hosts cpu",
286 - Ctx: "vsphere.host_cpu_utilization",
287 - Priority: prioHostCPUUtilization,
288 - Dims: collectorapi.Dims{
289 - {ID: "%s_cpu.usage.average", Name: "used", Div: 100},
290 - },
291 - }
292 - hostMemUtilizationChartTmpl = collectorapi.Chart{
293 - ID: "%s_mem_utilization",
294 - Title: "ESXi Host memory utilization",
295 - Units: "percentage",
296 - Fam: "hosts mem",
297 - Ctx: "vsphere.host_mem_utilization",
298 - Priority: prioHostMemoryUtilization,
299 - Dims: collectorapi.Dims{
300 - {ID: "%s_mem.usage.average", Name: "used", Div: 100},
301 - },
302 - }
303 - hostMemUsageChartTmpl = collectorapi.Chart{
304 - ID: "%s_mem_usage",
305 - Title: "ESXi Host memory usage",
306 - Units: "KiB",
307 - Fam: "hosts mem",
308 - Ctx: "vsphere.host_mem_usage",
309 - Priority: prioHostMemoryUsage,
310 - Dims: collectorapi.Dims{
311 - {ID: "%s_mem.granted.average", Name: "granted"},
312 - {ID: "%s_mem.consumed.average", Name: "consumed"},
313 - {ID: "%s_mem.active.average", Name: "active"},
314 - {ID: "%s_mem.shared.average", Name: "shared"},
315 - {ID: "%s_mem.sharedcommon.average", Name: "sharedcommon"},
316 - },
317 - }
318 - hostMemSwapIOChartTmpl = collectorapi.Chart{
319 - ID: "%s_mem_swap_rate",
320 - Title: "ESXi Host VMKernel memory swap IO",
321 - Units: "KiB/s",
322 - Fam: "hosts mem",
323 - Ctx: "vsphere.host_mem_swap_io",
324 - Type: collectorapi.Area,
325 - Priority: prioHostMemorySwapIO,
326 - Dims: collectorapi.Dims{
327 - {ID: "%s_mem.swapinRate.average", Name: "in"},
328 - {ID: "%s_mem.swapoutRate.average", Name: "out"},
329 - },
330 - }
331 -
332 - hostDiskIOChartTmpl = collectorapi.Chart{
333 - ID: "%s_disk_io",
334 - Title: "ESXi Host disk IO",
335 - Units: "KiB/s",
336 - Fam: "hosts disk",
337 - Ctx: "vsphere.host_disk_io",
338 - Type: collectorapi.Area,
339 - Priority: prioHostDiskIO,
340 - Dims: collectorapi.Dims{
341 - {ID: "%s_disk.read.average", Name: "read"},
342 - {ID: "%s_disk.write.average", Name: "write", Mul: -1},
343 - },
344 - }
345 - hostDiskMaxLatencyChartTmpl = collectorapi.Chart{
346 - ID: "%s_disk_max_latency",
347 - Title: "ESXi Host disk max latency",
348 - Units: "milliseconds",
349 - Fam: "hosts disk",
350 - Ctx: "vsphere.host_disk_max_latency",
351 - Priority: prioHostDiskMaxLatency,
352 - Dims: collectorapi.Dims{
353 - {ID: "%s_disk.maxTotalLatency.latest", Name: "latency"},
354 - },
355 - }
356 -
357 - hostNetworkTraffic = collectorapi.Chart{
358 - ID: "%s_net_traffic",
359 - Title: "ESXi Host network traffic",
360 - Units: "KiB/s",
361 - Fam: "hosts net",
362 - Ctx: "vsphere.host_net_traffic",
363 - Type: collectorapi.Area,
364 - Priority: prioHostNetworkTraffic,
365 - Dims: collectorapi.Dims{
366 - {ID: "%s_net.bytesRx.average", Name: "received"},
367 - {ID: "%s_net.bytesTx.average", Name: "sent", Mul: -1},
368 - },
369 - }
370 - hostNetworkPacketsChartTmpl = collectorapi.Chart{
371 - ID: "%s_net_packets",
372 - Title: "ESXi Host network packets",
373 - Units: "packets",
374 - Fam: "hosts net",
375 - Ctx: "vsphere.host_net_packets",
376 - Priority: prioHostNetworkPackets,
377 - Dims: collectorapi.Dims{
378 - {ID: "%s_net.packetsRx.summation", Name: "received"},
379 - {ID: "%s_net.packetsTx.summation", Name: "sent", Mul: -1},
380 - },
381 - }
382 - hostNetworkDropsChartTmpl = collectorapi.Chart{
383 - ID: "%s_net_drops_total",
384 - Title: "ESXi Host network drops",
385 - Units: "drops",
386 - Fam: "hosts net",
387 - Ctx: "vsphere.host_net_drops",
388 - Priority: prioHostNetworkDrops,
389 - Dims: collectorapi.Dims{
390 - {ID: "%s_net.droppedRx.summation", Name: "received"},
391 - {ID: "%s_net.droppedTx.summation", Name: "sent", Mul: -1},
392 - },
393 - }
394 - hostNetworkErrorsChartTmpl = collectorapi.Chart{
395 - ID: "%s_net_errors",
396 - Title: "ESXi Host network errors",
397 - Units: "errors",
398 - Fam: "hosts net",
399 - Ctx: "vsphere.host_net_errors",
400 - Priority: prioHostNetworkErrors,
401 - Dims: collectorapi.Dims{
402 - {ID: "%s_net.errorsRx.summation", Name: "received"},
403 - {ID: "%s_net.errorsTx.summation", Name: "sent", Mul: -1},
404 - },
405 - }
406 -
407 - hostOverallStatusChartTmpl = collectorapi.Chart{
408 - ID: "%s_overall_status",
409 - Title: "ESXi Host overall alarm status",
410 - Units: "status",
411 - Fam: "hosts status",
412 - Ctx: "vsphere.host_overall_status",
413 - Priority: prioHostOverallStatus,
414 - Dims: collectorapi.Dims{
415 - {ID: "%s_overall.status.green", Name: "green"},
416 - {ID: "%s_overall.status.red", Name: "red"},
417 - {ID: "%s_overall.status.yellow", Name: "yellow"},
418 - {ID: "%s_overall.status.gray", Name: "gray"},
419 - },
420 - }
421 - hostSystemUptimeChartTmpl = collectorapi.Chart{
422 - ID: "%s_system_uptime",
423 - Title: "ESXi Host system uptime",
424 - Units: "seconds",
425 - Fam: "hosts uptime",
426 - Ctx: "vsphere.host_system_uptime",
427 - Priority: prioHostSystemUptime,
428 - Dims: collectorapi.Dims{
429 - {ID: "%s_sys.uptime.latest", Name: "uptime"},
430 - },
431 - }
432 -)
433 -
434 -var (
435 - datastorePropertyChartsTmpl = collectorapi.Charts{
436 - datastoreSpaceUtilizationChartTmpl.Copy(),
437 - datastoreSpaceUsageChartTmpl.Copy(),
438 - datastoreOverallStatusChartTmpl.Copy(),
439 - }
440 - datastorePerfChartsTmpl = collectorapi.Charts{
441 - datastoreDiskIOChartTmpl.Copy(),
442 - datastoreDiskIOPSChartTmpl.Copy(),
443 - datastoreDiskLatencyChartTmpl.Copy(),
444 - }
445 -
446 - datastoreDiskIOChartTmpl = collectorapi.Chart{
447 - ID: "%s_disk_io",
448 - Title: "Datastore disk IO",
449 - Units: "KiB/s",
450 - Fam: "datastores disk",
451 - Ctx: "vsphere.datastore_disk_io",
452 - Type: collectorapi.Area,
453 - Priority: prioDatastoreDiskIO,
454 - Dims: collectorapi.Dims{
455 - {ID: "%s_datastore.read.average", Name: "read"},
456 - {ID: "%s_datastore.write.average", Name: "write", Mul: -1},
457 - },
458 - }
459 - datastoreDiskIOPSChartTmpl = collectorapi.Chart{
460 - ID: "%s_disk_iops",
461 - Title: "Datastore disk IOPS",
462 - Units: "operations/s",
463 - Fam: "datastores disk",
464 - Ctx: "vsphere.datastore_disk_iops",
465 - Priority: prioDatastoreDiskIOPS,
466 - Dims: collectorapi.Dims{
467 - {ID: "%s_datastore.numberReadAveraged.average", Name: "reads"},
468 - {ID: "%s_datastore.numberWriteAveraged.average", Name: "writes", Mul: -1},
469 - },
470 - }
471 - datastoreDiskLatencyChartTmpl = collectorapi.Chart{
472 - ID: "%s_disk_latency",
473 - Title: "Datastore disk latency",
474 - Units: "milliseconds",
475 - Fam: "datastores disk",
476 - Ctx: "vsphere.datastore_disk_latency",
477 - Priority: prioDatastoreDiskLatency,
478 - Dims: collectorapi.Dims{
479 - {ID: "%s_datastore.totalReadLatency.average", Name: "read"},
480 - {ID: "%s_datastore.totalWriteLatency.average", Name: "write"},
481 - },
482 - }
483 -
484 - datastoreSpaceUtilizationChartTmpl = collectorapi.Chart{
485 - ID: "%s_space_utilization",
486 - Title: "Datastore space utilization",
487 - Units: "percentage",
488 - Fam: "datastores space",
489 - Ctx: "vsphere.datastore_space_utilization",
490 - Priority: prioDatastoreSpaceUtilization,
491 - Dims: collectorapi.Dims{
492 - {ID: "%s_used_space_pct", Name: "used", Div: 100},
493 - },
494 - }
495 - datastoreSpaceUsageChartTmpl = collectorapi.Chart{
496 - ID: "%s_space_usage",
497 - Title: "Datastore space usage",
498 - Units: "bytes",
499 - Fam: "datastores space",
500 - Ctx: "vsphere.datastore_space_usage",
501 - Priority: prioDatastoreSpaceUsage,
502 - Dims: collectorapi.Dims{
503 - {ID: "%s_capacity", Name: "capacity"},
504 - {ID: "%s_free_space", Name: "free"},
505 - {ID: "%s_used_space", Name: "used"},
506 - },
507 - }
508 -
509 - datastoreOverallStatusChartTmpl = collectorapi.Chart{
510 - ID: "%s_overall_status",
511 - Title: "Datastore overall alarm status",
512 - Units: "status",
513 - Fam: "datastores status",
514 - Ctx: "vsphere.datastore_overall_status",
515 - Priority: prioDatastoreOverallStatus,
516 - Dims: collectorapi.Dims{
517 - {ID: "%s_overall.status.green", Name: "green"},
518 - {ID: "%s_overall.status.red", Name: "red"},
519 - {ID: "%s_overall.status.yellow", Name: "yellow"},
520 - {ID: "%s_overall.status.gray", Name: "gray"},
521 - },
522 - }
523 -)
524 -
525 -const failedUpdatesLimit = 10
526 -
527 -func (c *Collector) updateCharts() {
528 - for id, fails := range c.discoveredHosts {
529 - if fails >= failedUpdatesLimit {
530 - c.removeFromCharts(id)
531 - delete(c.charted, id)
532 - delete(c.discoveredHosts, id)
533 - continue
534 - }
535 -
536 - host := c.resources.Hosts.Get(id)
537 - if host == nil || c.charted[id] || fails != 0 {
538 - continue
539 - }
540 -
541 - c.charted[id] = true
542 - charts := newHostCharts(host)
543 - if err := c.Charts().Add(*charts...); err != nil {
544 - c.Error(err)
545 - }
546 - }
547 -
548 - for id, fails := range c.discoveredVMs {
549 - if fails >= failedUpdatesLimit {
550 - c.removeFromCharts(id)
551 - delete(c.charted, id)
552 - delete(c.discoveredVMs, id)
553 - continue
554 - }
555 -
556 - vm := c.resources.VMs.Get(id)
557 - if vm == nil || c.charted[id] || fails != 0 {
558 - continue
559 - }
560 -
561 - c.charted[id] = true
562 - charts := newVMCHarts(vm)
563 - if err := c.Charts().Add(*charts...); err != nil {
564 - c.Error(err)
565 - }
566 - }
567 -
568 - for id, fails := range c.discoveredDatastores {
569 - if fails >= failedUpdatesLimit {
570 - c.removeFromCharts(id)
571 - delete(c.charted, id)
572 - delete(c.discoveredDatastores, id)
573 - delete(c.datastorePerfReceived, id)
574 - delete(c.datastorePerfCharted, id)
575 - continue
576 - }
577 -
578 - ds := c.resources.Datastores.Get(id)
579 - if ds == nil || fails != 0 {
580 - continue
581 - }
582 -
583 - if !c.charted[id] {
584 - c.charted[id] = true
585 - charts := newDatastorePropertyCharts(ds)
586 - if err := c.Charts().Add(*charts...); err != nil {
587 - c.Error(err)
588 - }
589 - }
590 -
591 - if c.datastorePerfReceived[id] && !c.datastorePerfCharted[id] {
592 - c.datastorePerfCharted[id] = true
593 - charts := newDatastorePerfCharts(ds)
594 - if err := c.Charts().Add(*charts...); err != nil {
595 - c.Error(err)
596 - }
597 - }
598 - }
599 -
600 - for id, fails := range c.discoveredClusters {
601 - if fails >= failedUpdatesLimit {
602 - c.removeFromCharts(id)
603 - delete(c.charted, id)
604 - delete(c.discoveredClusters, id)
605 - delete(c.clusterPerfReceived, id)
606 - delete(c.clusterPerfCharted, id)
607 - continue
608 - }
609 -
610 - cl := c.resources.Clusters.Get(id)
611 - if cl == nil || fails != 0 {
612 - continue
613 - }
614 -
615 - if !c.charted[id] {
616 - c.charted[id] = true
617 - charts := newClusterPropertyCharts(cl)
618 - if err := c.Charts().Add(*charts...); err != nil {
619 - c.Error(err)
620 - }
621 - }
622 -
623 - if c.clusterPerfReceived[id] && !c.clusterPerfCharted[id] {
624 - c.clusterPerfCharted[id] = true
625 - charts := newClusterPerfCharts(cl)
626 - if err := c.Charts().Add(*charts...); err != nil {
627 - c.Error(err)
628 - }
629 - }
630 - }
631 -
632 - for id, fails := range c.discoveredResourcePools {
633 - if fails >= failedUpdatesLimit {
634 - c.removeFromCharts(id)
635 - delete(c.charted, id)
636 - delete(c.discoveredResourcePools, id)
637 - continue
638 - }
639 -
640 - rp := c.resources.ResourcePools.Get(id)
641 - if rp == nil || c.charted[id] || fails != 0 {
642 - continue
643 - }
644 -
645 - c.charted[id] = true
646 - charts := newResourcePoolCharts(rp)
647 - if err := c.Charts().Add(*charts...); err != nil {
648 - c.Error(err)
649 - }
650 - }
651 -}
652 -
653 -func newVMCHarts(vm *rs.VM) *collectorapi.Charts {
654 - charts := vmChartsTmpl.Copy()
655 -
656 - for _, chart := range *charts {
657 - chart.ID = fmt.Sprintf(chart.ID, vm.ID)
658 - chart.Labels = []collectorapi.Label{
659 - {Key: "datacenter", Value: vm.Hier.DC.Name},
660 - {Key: "cluster", Value: getVMClusterName(vm)},
661 - {Key: "host", Value: vm.Hier.Host.Name},
662 - {Key: "vm", Value: vm.Name},
663 - }
664 - for _, dim := range chart.Dims {
665 - dim.ID = fmt.Sprintf(dim.ID, vm.ID)
666 - }
667 - }
668 -
669 - return charts
670 -}
671 -
672 -func getVMClusterName(vm *rs.VM) string {
673 - if vm.Hier.Cluster.Name == vm.Hier.Host.Name {
674 - return ""
675 - }
676 - return vm.Hier.Cluster.Name
677 -}
678 -
679 -func newHostCharts(host *rs.Host) *collectorapi.Charts {
680 - charts := hostChartsTmpl.Copy()
681 -
682 - for _, chart := range *charts {
683 - chart.ID = fmt.Sprintf(chart.ID, host.ID)
684 - chart.Labels = []collectorapi.Label{
685 - {Key: "datacenter", Value: host.Hier.DC.Name},
686 - {Key: "cluster", Value: getHostClusterName(host)},
687 - {Key: "host", Value: host.Name},
688 - }
689 -
690 - for _, dim := range chart.Dims {
691 - dim.ID = fmt.Sprintf(dim.ID, host.ID)
692 - }
693 - }
694 -
695 - return charts
696 -}
697 -
698 -func getHostClusterName(host *rs.Host) string {
699 - if host.Hier.Cluster.Name == host.Name {
700 - return ""
701 - }
702 - return host.Hier.Cluster.Name
703 -}
704 -
705 -func newDatastorePropertyCharts(ds *rs.Datastore) *collectorapi.Charts {
706 - charts := datastorePropertyChartsTmpl.Copy()
707 - applyDatastoreChartLabels(charts, ds)
708 - return charts
709 -}
710 -
711 -func newDatastorePerfCharts(ds *rs.Datastore) *collectorapi.Charts {
712 - charts := datastorePerfChartsTmpl.Copy()
713 - applyDatastoreChartLabels(charts, ds)
714 - return charts
715 -}
716 -
717 -func applyDatastoreChartLabels(charts *collectorapi.Charts, ds *rs.Datastore) {
718 - for _, chart := range *charts {
719 - chart.ID = fmt.Sprintf(chart.ID, ds.ID)
720 - chart.Labels = []collectorapi.Label{
721 - {Key: "datacenter", Value: ds.Hier.DC.Name},
722 - {Key: "datastore", Value: ds.Name},
723 - {Key: "type", Value: ds.Type},
724 - }
725 - for _, dim := range chart.Dims {
726 - dim.ID = fmt.Sprintf(dim.ID, ds.ID)
727 - }
728 - }
729 -}
730 -
731 -// --- Cluster chart templates ---
732 -
733 -var (
734 - clusterPropertyChartsTmpl = collectorapi.Charts{
735 - clusterHostsChartTmpl.Copy(),
736 - clusterCPUCapacityChartTmpl.Copy(),
737 - clusterMemCapacityChartTmpl.Copy(),
738 - clusterCPUTopologyChartTmpl.Copy(),
739 - clusterDRSConfigChartTmpl.Copy(),
740 - clusterHAConfigChartTmpl.Copy(),
741 - clusterOverallStatusChartTmpl.Copy(),
742 - clusterVMotionsChartTmpl.Copy(),
743 - clusterDRSScoreChartTmpl.Copy(),
744 - clusterDRSBalanceChartTmpl.Copy(),
745 - clusterVMCountChartTmpl.Copy(),
746 - clusterUsageCPUChartTmpl.Copy(),
747 - clusterUsageMemChartTmpl.Copy(),
748 - }
749 - clusterPerfChartsTmpl = collectorapi.Charts{
750 - clusterCPUUtilizationChartTmpl.Copy(),
751 - clusterCPUUsageChartTmpl.Copy(),
752 - clusterMemUtilizationChartTmpl.Copy(),
753 - clusterMemUsageChartTmpl.Copy(),
754 - clusterServicesFairnessChartTmpl.Copy(),
755 - clusterServicesEffectiveCPUChartTmpl.Copy(),
756 - clusterServicesEffectiveMemChartTmpl.Copy(),
757 - clusterServicesFailoverChartTmpl.Copy(),
758 - clusterVMMigrationsChartTmpl.Copy(),
759 - clusterVMLifecycleChartTmpl.Copy(),
760 - clusterVMManagementChartTmpl.Copy(),
761 - clusterVMGuestOpsChartTmpl.Copy(),
762 - clusterVMColdMigrationsChartTmpl.Copy(),
763 - }
764 -
765 - // Property charts
766 - clusterHostsChartTmpl = collectorapi.Chart{
767 - ID: "%s_hosts",
768 - Title: "Cluster host count",
769 - Units: "hosts",
770 - Fam: "clusters hosts",
771 - Ctx: "vsphere.cluster_hosts",
772 - Priority: prioClusterHosts,
773 - Dims: collectorapi.Dims{
774 - {ID: "%s_num_hosts", Name: "total"},
775 - {ID: "%s_num_effective_hosts", Name: "effective"},
776 - },
777 - }
778 - clusterCPUCapacityChartTmpl = collectorapi.Chart{
779 - ID: "%s_cpu_capacity",
780 - Title: "Cluster CPU capacity",
781 - Units: "MHz",
782 - Fam: "clusters cpu",
783 - Ctx: "vsphere.cluster_cpu_capacity",
784 - Priority: prioClusterCPUCapacity,
785 - Dims: collectorapi.Dims{
786 - {ID: "%s_total_cpu", Name: "total"},
787 - {ID: "%s_effective_cpu", Name: "effective"},
788 - },
789 - }
790 - clusterMemCapacityChartTmpl = collectorapi.Chart{
791 - ID: "%s_mem_capacity",
792 - Title: "Cluster memory capacity",
793 - Units: "bytes",
794 - Fam: "clusters mem",
795 - Ctx: "vsphere.cluster_mem_capacity",
796 - Priority: prioClusterMemCapacity,
797 - Dims: collectorapi.Dims{
798 - {ID: "%s_total_memory", Name: "total"},
799 - {ID: "%s_effective_memory", Name: "effective"},
800 - },
801 - }
802 - clusterCPUTopologyChartTmpl = collectorapi.Chart{
803 - ID: "%s_cpu_topology",
804 - Title: "Cluster CPU topology",
805 - Units: "count",
806 - Fam: "clusters cpu",
807 - Ctx: "vsphere.cluster_cpu_topology",
808 - Priority: prioClusterCPUTopology,
809 - Dims: collectorapi.Dims{
810 - {ID: "%s_num_cpu_cores", Name: "cores"},
811 - {ID: "%s_num_cpu_threads", Name: "threads"},
812 - },
813 - }
814 - clusterDRSConfigChartTmpl = collectorapi.Chart{
815 - ID: "%s_drs_config",
816 - Title: "Cluster DRS enabled",
817 - Units: "status",
818 - Fam: "clusters config",
819 - Ctx: "vsphere.cluster_drs_config",
820 - Priority: prioClusterDRSConfig,
821 - Dims: collectorapi.Dims{
822 - {ID: "%s_drs_enabled", Name: "enabled"},
823 - },
824 - }
825 - clusterHAConfigChartTmpl = collectorapi.Chart{
826 - ID: "%s_ha_config",
827 - Title: "Cluster HA configuration",
828 - Units: "status",
829 - Fam: "clusters config",
830 - Ctx: "vsphere.cluster_ha_config",
831 - Priority: prioClusterHAConfig,
832 - Dims: collectorapi.Dims{
833 - {ID: "%s_ha_enabled", Name: "enabled"},
834 - {ID: "%s_ha_adm_ctrl_enabled", Name: "admission_control"},
835 - },
836 - }
837 - clusterOverallStatusChartTmpl = collectorapi.Chart{
838 - ID: "%s_overall_status",
839 - Title: "Cluster overall alarm status",
840 - Units: "status",
841 - Fam: "clusters status",
842 - Ctx: "vsphere.cluster_overall_status",
843 - Priority: prioClusterOverallStatus,
844 - Dims: collectorapi.Dims{
845 - {ID: "%s_overall.status.green", Name: "green"},
846 - {ID: "%s_overall.status.red", Name: "red"},
847 - {ID: "%s_overall.status.yellow", Name: "yellow"},
848 - {ID: "%s_overall.status.gray", Name: "gray"},
849 - },
850 - }
851 - clusterVMotionsChartTmpl = collectorapi.Chart{
852 - ID: "%s_vmotions",
853 - Title: "Cluster cumulative vMotion count",
854 - Units: "migrations",
855 - Fam: "clusters migrations",
856 - Ctx: "vsphere.cluster_vmotions",
857 - Type: collectorapi.Line,
858 - Priority: prioClusterVMotions,
859 - Dims: collectorapi.Dims{
860 - {ID: "%s_num_vmotions", Name: "vmotions", Algo: collectorapi.Incremental},
861 - },
862 - }
863 - clusterDRSScoreChartTmpl = collectorapi.Chart{
864 - ID: "%s_drs_score",
865 - Title: "Cluster DRS score",
866 - Units: "percentage",
867 - Fam: "clusters drs",
868 - Ctx: "vsphere.cluster_drs_score",
869 - Priority: prioClusterDRSScore,
870 - Dims: collectorapi.Dims{
871 - {ID: "%s_drs_score", Name: "score"},
872 - },
873 - }
874 - clusterDRSBalanceChartTmpl = collectorapi.Chart{
875 - ID: "%s_drs_balance",
876 - Title: "Cluster DRS load balance",
877 - Units: "score",
878 - Fam: "clusters drs",
879 - Ctx: "vsphere.cluster_drs_balance",
880 - Priority: prioClusterDRSBalance,
881 - Dims: collectorapi.Dims{
882 - {ID: "%s_current_balance", Name: "current", Div: 1000},
883 - {ID: "%s_target_balance", Name: "target", Div: 1000},
884 - },
885 - }
886 - clusterVMCountChartTmpl = collectorapi.Chart{
887 - ID: "%s_vm_count",
888 - Title: "Cluster VM count",
889 - Units: "VMs",
890 - Fam: "clusters vms",
891 - Ctx: "vsphere.cluster_vm_count",
892 - Priority: prioClusterVMCount,
893 - Dims: collectorapi.Dims{
894 - {ID: "%s_usage_total_vm_count", Name: "total"},
895 - {ID: "%s_usage_powered_off_vm_count", Name: "powered_off"},
896 - },
897 - }
898 - clusterUsageCPUChartTmpl = collectorapi.Chart{
899 - ID: "%s_usage_cpu",
900 - Title: "Cluster DRS CPU usage summary",
901 - Units: "MHz",
902 - Fam: "clusters cpu",
903 - Ctx: "vsphere.cluster_usage_cpu",
904 - Priority: prioClusterUsageCPU,
905 - Dims: collectorapi.Dims{
906 - {ID: "%s_usage_cpu_demand_mhz", Name: "demand"},
907 - {ID: "%s_usage_cpu_entitled_mhz", Name: "entitled"},
908 - {ID: "%s_usage_cpu_reservation_mhz", Name: "reserved"},
909 - },
910 - }
911 - clusterUsageMemChartTmpl = collectorapi.Chart{
912 - ID: "%s_usage_mem",
913 - Title: "Cluster DRS memory usage summary",
914 - Units: "MB",
915 - Fam: "clusters mem",
916 - Ctx: "vsphere.cluster_usage_mem",
917 - Priority: prioClusterUsageMem,
918 - Dims: collectorapi.Dims{
919 - {ID: "%s_usage_mem_demand_mb", Name: "demand"},
920 - {ID: "%s_usage_mem_entitled_mb", Name: "entitled"},
921 - {ID: "%s_usage_mem_reservation_mb", Name: "reserved"},
922 - },
923 - }
924 -
925 - // Perf charts (created only when perf data arrives)
926 - clusterCPUUtilizationChartTmpl = collectorapi.Chart{
927 - ID: "%s_cpu_utilization",
928 - Title: "Cluster CPU utilization",
929 - Units: "percentage",
930 - Fam: "clusters cpu",
931 - Ctx: "vsphere.cluster_cpu_utilization",
932 - Priority: prioClusterCPUUtilization,
933 - Dims: collectorapi.Dims{
934 - {ID: "%s_cpu.usage.average", Name: "used", Div: 100},
935 - },
936 - }
937 - clusterCPUUsageChartTmpl = collectorapi.Chart{
938 - ID: "%s_cpu_usage_mhz",
939 - Title: "Cluster CPU usage",
940 - Units: "MHz",
941 - Fam: "clusters cpu",
942 - Ctx: "vsphere.cluster_cpu_usage",
943 - Priority: prioClusterCPUUsage,
944 - Dims: collectorapi.Dims{
945 - {ID: "%s_cpu.usagemhz.average", Name: "used"},
946 - {ID: "%s_cpu.totalmhz.average", Name: "total"},
947 - },
948 - }
949 - clusterMemUtilizationChartTmpl = collectorapi.Chart{
950 - ID: "%s_mem_utilization",
951 - Title: "Cluster memory utilization",
952 - Units: "percentage",
953 - Fam: "clusters mem",
954 - Ctx: "vsphere.cluster_mem_utilization",
955 - Priority: prioClusterMemUtilization,
956 - Dims: collectorapi.Dims{
957 - {ID: "%s_mem.usage.average", Name: "used", Div: 100},
958 - },
959 - }
960 - clusterMemUsageChartTmpl = collectorapi.Chart{
961 - ID: "%s_mem_usage",
962 - Title: "Cluster memory usage",
963 - Units: "KiB",
964 - Fam: "clusters mem",
965 - Ctx: "vsphere.cluster_mem_usage",
966 - Priority: prioClusterMemUsage,
967 - Dims: collectorapi.Dims{
968 - {ID: "%s_mem.consumed.average", Name: "consumed"},
969 - {ID: "%s_mem.active.average", Name: "active"},
970 - {ID: "%s_mem.granted.average", Name: "granted"},
971 - {ID: "%s_mem.shared.average", Name: "shared"},
972 - {ID: "%s_mem.overhead.average", Name: "overhead"},
973 - {ID: "%s_mem.swapused.average", Name: "swap_used"},
974 - },
975 - }
976 - clusterServicesFairnessChartTmpl = collectorapi.Chart{
977 - ID: "%s_services_fairness",
978 - Title: "Cluster DRS resource distribution fairness",
979 - Units: "score",
980 - Fam: "clusters drs",
981 - Ctx: "vsphere.cluster_services_fairness",
982 - Priority: prioClusterServicesFairness,
983 - Dims: collectorapi.Dims{
984 - {ID: "%s_clusterServices.cpufairness.latest", Name: "cpu"},
985 - {ID: "%s_clusterServices.memfairness.latest", Name: "memory"},
986 - },
987 - }
988 - clusterServicesEffectiveCPUChartTmpl = collectorapi.Chart{
989 - ID: "%s_services_effective_cpu",
990 - Title: "Cluster effective CPU capacity",
991 - Units: "MHz",
992 - Fam: "clusters cpu",
993 - Ctx: "vsphere.cluster_services_effective_cpu",
994 - Priority: prioClusterServicesEffectiveCPU,
995 - Dims: collectorapi.Dims{
996 - {ID: "%s_clusterServices.effectivecpu.average", Name: "effective_cpu"},
997 - },
998 - }
999 - clusterServicesEffectiveMemChartTmpl = collectorapi.Chart{
1000 - ID: "%s_services_effective_mem",
1001 - Title: "Cluster effective memory capacity",
1002 - Units: "MB",
1003 - Fam: "clusters mem",
1004 - Ctx: "vsphere.cluster_services_effective_mem",
1005 - Priority: prioClusterServicesEffectiveMem,
1006 - Dims: collectorapi.Dims{
1007 - {ID: "%s_clusterServices.effectivemem.average", Name: "effective_mem"},
1008 - },
1009 - }
1010 - clusterServicesFailoverChartTmpl = collectorapi.Chart{
1011 - ID: "%s_services_failover",
1012 - Title: "Cluster HA failover capacity",
1013 - Units: "failures",
1014 - Fam: "clusters ha",
1015 - Ctx: "vsphere.cluster_services_failover",
1016 - Priority: prioClusterServicesFailover,
1017 - Dims: collectorapi.Dims{
1018 - {ID: "%s_clusterServices.failover.latest", Name: "failures_tolerable"},
1019 - },
1020 - }
1021 - clusterVMMigrationsChartTmpl = collectorapi.Chart{
1022 - ID: "%s_vm_migrations",
1023 - Title: "Cluster VM migration operations",
1024 - Units: "operations",
1025 - Fam: "clusters vmop",
1026 - Ctx: "vsphere.cluster_vm_migrations",
1027 - Priority: prioClusterVMMigrations,
1028 - Dims: collectorapi.Dims{
1029 - {ID: "%s_vmop.numVMotion.latest", Name: "vmotion"},
1030 - {ID: "%s_vmop.numSVMotion.latest", Name: "svmotion"},
1031 - {ID: "%s_vmop.numXVMotion.latest", Name: "xvmotion"},
1032 - },
1033 - }
1034 - clusterVMLifecycleChartTmpl = collectorapi.Chart{
1035 - ID: "%s_vm_lifecycle",
1036 - Title: "Cluster VM lifecycle operations",
1037 - Units: "operations",
1038 - Fam: "clusters vmop",
1039 - Ctx: "vsphere.cluster_vm_lifecycle",
1040 - Priority: prioClusterVMLifecycle,
1041 - Dims: collectorapi.Dims{
1042 - {ID: "%s_vmop.numPoweron.latest", Name: "poweron"},
1043 - {ID: "%s_vmop.numPoweroff.latest", Name: "poweroff"},
1044 - {ID: "%s_vmop.numCreate.latest", Name: "create"},
1045 - {ID: "%s_vmop.numDestroy.latest", Name: "destroy"},
1046 - {ID: "%s_vmop.numClone.latest", Name: "clone"},
1047 - {ID: "%s_vmop.numDeploy.latest", Name: "deploy"},
1048 - },
1049 - }
1050 - clusterVMManagementChartTmpl = collectorapi.Chart{
1051 - ID: "%s_vm_management",
1052 - Title: "Cluster VM management operations",
1053 - Units: "operations",
1054 - Fam: "clusters vmop",
1055 - Ctx: "vsphere.cluster_vm_management",
1056 - Priority: prioClusterVMManagement,
1057 - Dims: collectorapi.Dims{
1058 - {ID: "%s_vmop.numReconfigure.latest", Name: "reconfigure"},
1059 - {ID: "%s_vmop.numReset.latest", Name: "reset"},
1060 - {ID: "%s_vmop.numSuspend.latest", Name: "suspend"},
1061 - {ID: "%s_vmop.numRegister.latest", Name: "register"},
1062 - {ID: "%s_vmop.numUnregister.latest", Name: "unregister"},
1063 - },
1064 - }
1065 - clusterVMGuestOpsChartTmpl = collectorapi.Chart{
1066 - ID: "%s_vm_guest_ops",
1067 - Title: "Cluster VM guest operations",
1068 - Units: "operations",
1069 - Fam: "clusters vmop",
1070 - Ctx: "vsphere.cluster_vm_guest_ops",
1071 - Priority: prioClusterVMGuestOps,
1072 - Dims: collectorapi.Dims{
1073 - {ID: "%s_vmop.numRebootGuest.latest", Name: "reboot"},
1074 - {ID: "%s_vmop.numShutdownGuest.latest", Name: "shutdown"},
1075 - {ID: "%s_vmop.numStandbyGuest.latest", Name: "standby"},
1076 - },
1077 - }
1078 - clusterVMColdMigrationsChartTmpl = collectorapi.Chart{
1079 - ID: "%s_vm_cold_migrations",
1080 - Title: "Cluster VM cold migration operations",
1081 - Units: "operations",
1082 - Fam: "clusters vmop",
1083 - Ctx: "vsphere.cluster_vm_cold_migrations",
1084 - Priority: prioClusterVMColdMigrations,
1085 - Dims: collectorapi.Dims{
1086 - {ID: "%s_vmop.numChangeDS.latest", Name: "change_ds"},
1087 - {ID: "%s_vmop.numChangeHost.latest", Name: "change_host"},
1088 - {ID: "%s_vmop.numChangeHostDS.latest", Name: "change_host_ds"},
1089 - },
1090 - }
1091 -)
1092 -
1093 -// --- Resource Pool chart templates ---
1094 -
1095 -var (
1096 - resourcePoolChartsTmpl = collectorapi.Charts{
1097 - rpCPUUsageChartTmpl.Copy(),
1098 - rpCPUEntitlementChartTmpl.Copy(),
1099 - rpCPUAllocationChartTmpl.Copy(),
1100 - rpMemUsageChartTmpl.Copy(),
1101 - rpMemEntitlementChartTmpl.Copy(),
1102 - rpMemAllocationChartTmpl.Copy(),
1103 - rpMemBreakdownChartTmpl.Copy(),
1104 - rpCPUConfigChartTmpl.Copy(),
1105 - rpMemConfigChartTmpl.Copy(),
1106 - rpOverallStatusChartTmpl.Copy(),
1107 - }
1108 -
1109 - rpCPUUsageChartTmpl = collectorapi.Chart{
1110 - ID: "%s_cpu_usage",
1111 - Title: "Resource Pool CPU usage vs demand",
1112 - Units: "MHz",
1113 - Fam: "resource pools cpu",
1114 - Ctx: "vsphere.resource_pool_cpu_usage",
1115 - Priority: prioResourcePoolCPUUsage,
1116 - Dims: collectorapi.Dims{
1117 - {ID: "%s_cpu_usage", Name: "usage"},
1118 - {ID: "%s_cpu_demand", Name: "demand"},
1119 - },
1120 - }
1121 - rpCPUEntitlementChartTmpl = collectorapi.Chart{
1122 - ID: "%s_cpu_entitlement",
1123 - Title: "Resource Pool CPU entitlement",
1124 - Units: "MHz",
1125 - Fam: "resource pools cpu",
1126 - Ctx: "vsphere.resource_pool_cpu_entitlement",
1127 - Priority: prioResourcePoolCPUEntitlement,
1128 - Dims: collectorapi.Dims{
1129 - {ID: "%s_cpu_entitlement_distributed", Name: "distributed"},
1130 - },
1131 - }
1132 - rpCPUAllocationChartTmpl = collectorapi.Chart{
1133 - ID: "%s_cpu_allocation",
1134 - Title: "Resource Pool CPU allocation",
1135 - Units: "MHz",
1136 - Fam: "resource pools cpu",
1137 - Ctx: "vsphere.resource_pool_cpu_allocation",
1138 - Priority: prioResourcePoolCPUAllocation,
1139 - Dims: collectorapi.Dims{
1140 - {ID: "%s_cpu_reservation_used", Name: "reservation_used"},
1141 - {ID: "%s_cpu_unreserved_for_vm", Name: "unreserved_for_vm"},
1142 - {ID: "%s_cpu_max_usage", Name: "max_usage"},
1143 - },
1144 - }
1145 - rpMemUsageChartTmpl = collectorapi.Chart{
1146 - ID: "%s_mem_usage",
1147 - Title: "Resource Pool memory usage",
1148 - Units: "MB",
1149 - Fam: "resource pools mem",
1150 - Ctx: "vsphere.resource_pool_mem_usage",
1151 - Priority: prioResourcePoolMemUsage,
1152 - Dims: collectorapi.Dims{
1153 - {ID: "%s_mem_usage_host", Name: "host"},
1154 - {ID: "%s_mem_usage_guest", Name: "guest"},
1155 - },
1156 - }
1157 - rpMemEntitlementChartTmpl = collectorapi.Chart{
1158 - ID: "%s_mem_entitlement",
1159 - Title: "Resource Pool memory entitlement",
1160 - Units: "MB",
1161 - Fam: "resource pools mem",
1162 - Ctx: "vsphere.resource_pool_mem_entitlement",
1163 - Priority: prioResourcePoolMemEntitlement,
1164 - Dims: collectorapi.Dims{
1165 - {ID: "%s_mem_entitlement_distributed", Name: "distributed"},
1166 - },
1167 - }
1168 - rpMemAllocationChartTmpl = collectorapi.Chart{
1169 - ID: "%s_mem_allocation",
1170 - Title: "Resource Pool memory allocation",
1171 - Units: "bytes",
1172 - Fam: "resource pools mem",
1173 - Ctx: "vsphere.resource_pool_mem_allocation",
1174 - Priority: prioResourcePoolMemAllocation,
1175 - Dims: collectorapi.Dims{
1176 - {ID: "%s_mem_reservation_used", Name: "reservation_used"},
1177 - {ID: "%s_mem_unreserved_for_vm", Name: "unreserved_for_vm"},
1178 - {ID: "%s_mem_max_usage", Name: "max_usage"},
1179 - },
1180 - }
1181 - rpMemBreakdownChartTmpl = collectorapi.Chart{
1182 - ID: "%s_mem_breakdown",
1183 - Title: "Resource Pool memory state breakdown",
1184 - Units: "MB",
1185 - Fam: "resource pools mem",
1186 - Ctx: "vsphere.resource_pool_mem_breakdown",
1187 - Priority: prioResourcePoolMemBreakdown,
1188 - Dims: collectorapi.Dims{
1189 - {ID: "%s_mem_private", Name: "private"},
1190 - {ID: "%s_mem_shared", Name: "shared"},
1191 - {ID: "%s_mem_swapped", Name: "swapped"},
1192 - {ID: "%s_mem_ballooned", Name: "ballooned"},
1193 - {ID: "%s_mem_overhead", Name: "overhead"},
1194 - {ID: "%s_mem_consumed_overhead", Name: "consumed_overhead"},
1195 - {ID: "%s_mem_compressed", Name: "compressed", Div: 1024},
1196 - },
1197 - }
1198 - rpCPUConfigChartTmpl = collectorapi.Chart{
1199 - ID: "%s_cpu_config",
1200 - Title: "Resource Pool CPU configured reservation and limit",
1201 - Units: "MHz",
1202 - Fam: "resource pools cpu",
1203 - Ctx: "vsphere.resource_pool_cpu_config",
1204 - Priority: prioResourcePoolCPUConfig,
1205 - Dims: collectorapi.Dims{
1206 - {ID: "%s_cpu_reservation", Name: "reservation"},
1207 - {ID: "%s_cpu_limit", Name: "limit"},
1208 - },
1209 - }
1210 - rpMemConfigChartTmpl = collectorapi.Chart{
1211 - ID: "%s_mem_config",
1212 - Title: "Resource Pool memory configured reservation and limit",
1213 - Units: "MB",
1214 - Fam: "resource pools mem",
1215 - Ctx: "vsphere.resource_pool_mem_config",
1216 - Priority: prioResourcePoolMemConfig,
1217 - Dims: collectorapi.Dims{
1218 - {ID: "%s_mem_reservation", Name: "reservation"},
1219 - {ID: "%s_mem_limit", Name: "limit"},
1220 - },
1221 - }
1222 - rpOverallStatusChartTmpl = collectorapi.Chart{
1223 - ID: "%s_overall_status",
1224 - Title: "Resource Pool overall alarm status",
1225 - Units: "status",
1226 - Fam: "resource pools status",
1227 - Ctx: "vsphere.resource_pool_overall_status",
1228 - Priority: prioResourcePoolOverallStatus,
1229 - Dims: collectorapi.Dims{
1230 - {ID: "%s_overall.status.green", Name: "green"},
1231 - {ID: "%s_overall.status.red", Name: "red"},
1232 - {ID: "%s_overall.status.yellow", Name: "yellow"},
1233 - {ID: "%s_overall.status.gray", Name: "gray"},
1234 - },
1235 - }
1236 -)
1237 -
1238 -func newClusterPropertyCharts(cl *rs.Cluster) *collectorapi.Charts {
1239 - charts := clusterPropertyChartsTmpl.Copy()
1240 - applyClusterChartLabels(charts, cl)
1241 - return charts
1242 -}
1243 -
1244 -func newClusterPerfCharts(cl *rs.Cluster) *collectorapi.Charts {
1245 - charts := clusterPerfChartsTmpl.Copy()
1246 - applyClusterChartLabels(charts, cl)
1247 - return charts
1248 -}
1249 -
1250 -func applyClusterChartLabels(charts *collectorapi.Charts, cl *rs.Cluster) {
1251 - for _, chart := range *charts {
1252 - chart.ID = fmt.Sprintf(chart.ID, cl.ID)
1253 - chart.Labels = []collectorapi.Label{
1254 - {Key: "datacenter", Value: cl.Hier.DC.Name},
1255 - {Key: "cluster", Value: cl.Name},
1256 - }
1257 - for _, dim := range chart.Dims {
1258 - dim.ID = fmt.Sprintf(dim.ID, cl.ID)
1259 - }
1260 - }
1261 -}
1262 -
1263 -func newResourcePoolCharts(rp *rs.ResourcePool) *collectorapi.Charts {
1264 - charts := resourcePoolChartsTmpl.Copy()
1265 - for _, chart := range *charts {
1266 - chart.ID = fmt.Sprintf(chart.ID, rp.ID)
1267 - chart.Labels = []collectorapi.Label{
1268 - {Key: "datacenter", Value: rp.Hier.DC.Name},
1269 - {Key: "cluster", Value: rp.Hier.Cluster.Name},
1270 - {Key: "resource_pool", Value: rp.Name},
1271 - }
1272 - for _, dim := range chart.Dims {
1273 - dim.ID = fmt.Sprintf(dim.ID, rp.ID)
1274 - }
1275 - }
1276 - return charts
1277 -}
1278 -
1279 -func (c *Collector) removeFromCharts(prefix string) {
1280 - for _, c := range *c.Charts() {
1281 - if strings.HasPrefix(c.ID, prefix+"_") {
1282 - c.MarkRemove()
1283 - c.MarkNotCreated()
1284 - }
1285 - }
1286 -}
1287 -
1288 -//func findMetricSeriesByPrefix(ms []performance.MetricSeries, prefix string) []performance.MetricSeries {
1289 -// from := sort.Search(len(ms), func(i int) bool { return ms[i].Name >= prefix })
1290 -//
1291 -// if from == len(ms) || !strings.HasPrefix(ms[from].Name, prefix) {
1292 -// return nil
1293 -// }
1294 -//
1295 -// until := from + 1
1296 -// for until < len(ms) && strings.HasPrefix(ms[until].Name, prefix) {
1297 -// until++
1298 -// }
1299 -// return ms[from:until]
1300 -//}
src/go/plugin/go.d/collector/vsphere/charts.yaml new
+2332
@@ -0,0 +1,2332 @@
1 +version: v1
2 +context_namespace: vsphere
3 +groups:
4 + - family: inventory
5 + metrics:
6 + - inventory_objects_datacenters
7 + - inventory_objects_folders
8 + - inventory_objects_clusters
9 + - inventory_objects_hosts
10 + - inventory_objects_vms
11 + - inventory_objects_datastores
12 + - inventory_objects_resource_pools
13 + chart_defaults:
14 + instances:
15 + by_labels:
16 + - id
17 + charts:
18 + - id: inventory_objects
19 + title: vSphere inventory object count
20 + context: inventory_objects
21 + units: objects
22 + algorithm: absolute
23 + type: line
24 + priority: 70089
25 + lifecycle:
26 + expire_after_cycles: 10
27 + dimensions:
28 + - selector: inventory_objects_datacenters
29 + name: datacenters
30 + - selector: inventory_objects_folders
31 + name: folders
32 + - selector: inventory_objects_clusters
33 + name: clusters
34 + - selector: inventory_objects_hosts
35 + name: hosts
36 + - selector: inventory_objects_vms
37 + name: vms
38 + - selector: inventory_objects_datastores
39 + name: datastores
40 + - selector: inventory_objects_resource_pools
41 + name: resource_pools
42 + - family: vms cpu
43 + metrics:
44 + - vm_cpu_utilization_used
45 + chart_defaults:
46 + instances:
47 + by_labels:
48 + - id
49 + charts:
50 + - id: vm_cpu_utilization
51 + title: Virtual Machine CPU utilization
52 + context: vm_cpu_utilization
53 + units: percentage
54 + algorithm: absolute
55 + type: line
56 + priority: 70042
57 + lifecycle:
58 + expire_after_cycles: 10
59 + dimensions:
60 + - selector: vm_cpu_utilization_used
61 + name: used
62 + options:
63 + divisor: 100
64 + - family: vms mem
65 + metrics:
66 + - vm_mem_utilization_used
67 + - vm_mem_usage_granted
68 + - vm_mem_usage_consumed
69 + - vm_mem_usage_active
70 + - vm_mem_usage_shared
71 + - vm_mem_swap_usage_swapped
72 + - vm_mem_swap_io_in
73 + - vm_mem_swap_io_out
74 + chart_defaults:
75 + instances:
76 + by_labels:
77 + - id
78 + charts:
79 + - id: vm_mem_utilization
80 + title: Virtual Machine memory utilization
81 + context: vm_mem_utilization
82 + units: percentage
83 + algorithm: absolute
84 + type: line
85 + priority: 70043
86 + lifecycle:
87 + expire_after_cycles: 10
88 + dimensions:
89 + - selector: vm_mem_utilization_used
90 + name: used
91 + options:
92 + divisor: 100
93 + - id: vm_mem_usage
94 + title: Virtual Machine memory usage
95 + context: vm_mem_usage
96 + units: KiB
97 + algorithm: absolute
98 + type: line
99 + priority: 70044
100 + lifecycle:
101 + expire_after_cycles: 10
102 + dimensions:
103 + - selector: vm_mem_usage_granted
104 + name: granted
105 + - selector: vm_mem_usage_consumed
106 + name: consumed
107 + - selector: vm_mem_usage_active
108 + name: active
109 + - selector: vm_mem_usage_shared
110 + name: shared
111 + - id: vm_mem_swap_usage
112 + title: Virtual Machine VMKernel memory swap usage
113 + context: vm_mem_swap_usage
114 + units: KiB
115 + algorithm: absolute
116 + type: line
117 + priority: 70045
118 + lifecycle:
119 + expire_after_cycles: 10
120 + dimensions:
121 + - selector: vm_mem_swap_usage_swapped
122 + name: swapped
123 + - id: vm_mem_swap_io
124 + title: Virtual Machine VMKernel memory swap IO
125 + context: vm_mem_swap_io
126 + units: KiB/s
127 + algorithm: absolute
128 + type: area
129 + priority: 70046
130 + lifecycle:
131 + expire_after_cycles: 10
132 + dimensions:
133 + - selector: vm_mem_swap_io_in
134 + name: in
135 + - selector: vm_mem_swap_io_out
136 + name: out
137 + - family: vms disk
138 + metrics:
139 + - vm_disk_io_read
140 + - vm_disk_io_write
141 + - vm_disk_max_latency_latency
142 + chart_defaults:
143 + instances:
144 + by_labels:
145 + - id
146 + charts:
147 + - id: vm_disk_io
148 + title: Virtual Machine disk IO
149 + context: vm_disk_io
150 + units: KiB/s
151 + algorithm: absolute
152 + type: area
153 + priority: 70047
154 + lifecycle:
155 + expire_after_cycles: 10
156 + dimensions:
157 + - selector: vm_disk_io_read
158 + name: read
159 + - selector: vm_disk_io_write
160 + name: write
161 + options:
162 + multiplier: -1
163 + - id: vm_disk_max_latency
164 + title: Virtual Machine disk max latency
165 + context: vm_disk_max_latency
166 + units: milliseconds
167 + algorithm: absolute
168 + type: line
169 + priority: 70048
170 + lifecycle:
171 + expire_after_cycles: 10
172 + dimensions:
173 + - selector: vm_disk_max_latency_latency
174 + name: latency
175 + - family: vms net
176 + metrics:
177 + - vm_net_traffic_received
178 + - vm_net_traffic_sent
179 + - vm_net_packets_received
180 + - vm_net_packets_sent
181 + - vm_net_drops_received
182 + - vm_net_drops_sent
183 + chart_defaults:
184 + instances:
185 + by_labels:
186 + - id
187 + charts:
188 + - id: vm_net_traffic
189 + title: Virtual Machine network traffic
190 + context: vm_net_traffic
191 + units: KiB/s
192 + algorithm: absolute
193 + type: area
194 + priority: 70049
195 + lifecycle:
196 + expire_after_cycles: 10
197 + dimensions:
198 + - selector: vm_net_traffic_received
199 + name: received
200 + - selector: vm_net_traffic_sent
201 + name: sent
202 + options:
203 + multiplier: -1
204 + - id: vm_net_packets
205 + title: Virtual Machine network packets
206 + context: vm_net_packets
207 + units: packets
208 + algorithm: absolute
209 + type: line
210 + priority: 70050
211 + lifecycle:
212 + expire_after_cycles: 10
213 + dimensions:
214 + - selector: vm_net_packets_received
215 + name: received
216 + - selector: vm_net_packets_sent
217 + name: sent
218 + options:
219 + multiplier: -1
220 + - id: vm_net_drops
221 + title: Virtual Machine network dropped packets
222 + context: vm_net_drops
223 + units: drops
224 + algorithm: absolute
225 + type: line
226 + priority: 70051
227 + lifecycle:
228 + expire_after_cycles: 10
229 + dimensions:
230 + - selector: vm_net_drops_received
231 + name: received
232 + - selector: vm_net_drops_sent
233 + name: sent
234 + options:
235 + multiplier: -1
236 + - family: vms status
237 + metrics:
238 + - vm_overall_status_green
239 + - vm_overall_status_red
240 + - vm_overall_status_yellow
241 + - vm_overall_status_gray
242 + - vm_power_state_powered_on
243 + - vm_power_state_powered_off
244 + - vm_power_state_suspended
245 + - vm_connection_state_connected
246 + - vm_connection_state_disconnected
247 + - vm_connection_state_orphaned
248 + - vm_connection_state_inaccessible
249 + - vm_connection_state_invalid
250 + - vm_tools_running_status_running
251 + - vm_tools_running_status_not_running
252 + - vm_tools_running_status_executing_scripts
253 + - vm_tools_running_status_unknown
254 + - vm_tools_version_status_current
255 + - vm_tools_version_status_need_upgrade
256 + - vm_tools_version_status_not_installed
257 + - vm_tools_version_status_unmanaged
258 + - vm_tools_version_status_too_old
259 + - vm_tools_version_status_supported_old
260 + - vm_tools_version_status_supported_new
261 + - vm_tools_version_status_too_new
262 + - vm_tools_version_status_blacklisted
263 + - vm_tools_version_status_unknown
264 + - vm_consolidation_needed_needed
265 + - vm_consolidation_needed_not_needed
266 + chart_defaults:
267 + instances:
268 + by_labels:
269 + - id
270 + charts:
271 + - id: vm_overall_status
272 + title: Virtual Machine overall alarm status
273 + context: vm_overall_status
274 + units: status
275 + algorithm: absolute
276 + type: line
277 + priority: 70052
278 + lifecycle:
279 + expire_after_cycles: 10
280 + dimensions:
281 + - selector: vm_overall_status_green
282 + name: green
283 + - selector: vm_overall_status_red
284 + name: red
285 + - selector: vm_overall_status_yellow
286 + name: yellow
287 + - selector: vm_overall_status_gray
288 + name: gray
289 + - id: vm_power_state
290 + title: Virtual Machine power state
291 + context: vm_power_state
292 + units: status
293 + algorithm: absolute
294 + type: line
295 + priority: 70072
296 + lifecycle:
297 + expire_after_cycles: 10
298 + dimensions:
299 + - selector: vm_power_state_powered_on
300 + name: powered_on
301 + - selector: vm_power_state_powered_off
302 + name: powered_off
303 + - selector: vm_power_state_suspended
304 + name: suspended
305 + - id: vm_connection_state
306 + title: Virtual Machine connection state
307 + context: vm_connection_state
308 + units: status
309 + algorithm: absolute
310 + type: line
311 + priority: 70074
312 + lifecycle:
313 + expire_after_cycles: 10
314 + dimensions:
315 + - selector: vm_connection_state_connected
316 + name: connected
317 + - selector: vm_connection_state_disconnected
318 + name: disconnected
319 + - selector: vm_connection_state_orphaned
320 + name: orphaned
321 + - selector: vm_connection_state_inaccessible
322 + name: inaccessible
323 + - selector: vm_connection_state_invalid
324 + name: invalid
325 + - id: vm_tools_running_status
326 + title: Virtual Machine VMware Tools running status
327 + context: vm_tools_running_status
328 + units: status
329 + algorithm: absolute
330 + type: line
331 + priority: 70075
332 + lifecycle:
333 + expire_after_cycles: 10
334 + dimensions:
335 + - selector: vm_tools_running_status_running
336 + name: running
337 + - selector: vm_tools_running_status_not_running
338 + name: not_running
339 + - selector: vm_tools_running_status_executing_scripts
340 + name: executing_scripts
341 + - selector: vm_tools_running_status_unknown
342 + name: unknown
343 + - id: vm_tools_version_status
344 + title: Virtual Machine VMware Tools version status
345 + context: vm_tools_version_status
346 + units: status
347 + algorithm: absolute
348 + type: line
349 + priority: 70076
350 + lifecycle:
351 + expire_after_cycles: 10
352 + dimensions:
353 + - selector: vm_tools_version_status_current
354 + name: current
355 + - selector: vm_tools_version_status_need_upgrade
356 + name: need_upgrade
357 + - selector: vm_tools_version_status_not_installed
358 + name: not_installed
359 + - selector: vm_tools_version_status_unmanaged
360 + name: unmanaged
361 + - selector: vm_tools_version_status_too_old
362 + name: too_old
363 + - selector: vm_tools_version_status_supported_old
364 + name: supported_old
365 + - selector: vm_tools_version_status_supported_new
366 + name: supported_new
367 + - selector: vm_tools_version_status_too_new
368 + name: too_new
369 + - selector: vm_tools_version_status_blacklisted
370 + name: blacklisted
371 + - selector: vm_tools_version_status_unknown
372 + name: unknown
373 + - id: vm_consolidation_needed
374 + title: Virtual Machine disk consolidation status
375 + context: vm_consolidation_needed
376 + units: status
377 + algorithm: absolute
378 + type: line
379 + priority: 70077
380 + lifecycle:
381 + expire_after_cycles: 10
382 + dimensions:
383 + - selector: vm_consolidation_needed_needed
384 + name: needed
385 + - selector: vm_consolidation_needed_not_needed
386 + name: not_needed
387 + - family: vms uptime
388 + metrics:
389 + - vm_system_uptime_uptime
390 + chart_defaults:
391 + instances:
392 + by_labels:
393 + - id
394 + charts:
395 + - id: vm_system_uptime
396 + title: Virtual Machine system uptime
397 + context: vm_system_uptime
398 + units: seconds
399 + algorithm: absolute
400 + type: line
401 + priority: 70053
402 + lifecycle:
403 + expire_after_cycles: 10
404 + dimensions:
405 + - selector: vm_system_uptime_uptime
406 + name: uptime
407 + - family: vms config
408 + metrics:
409 + - vm_config_cpu_vcpus
410 + - vm_config_memory_memory
411 + - vm_config_devices_disks
412 + - vm_config_devices_nics
413 + chart_defaults:
414 + instances:
415 + by_labels:
416 + - id
417 + charts:
418 + - id: vm_config_cpu
419 + title: Virtual Machine configured CPU
420 + context: vm_config_cpu
421 + units: vCPUs
422 + algorithm: absolute
423 + type: line
424 + priority: 70078
425 + lifecycle:
426 + expire_after_cycles: 10
427 + dimensions:
428 + - selector: vm_config_cpu_vcpus
429 + name: vcpus
430 + - id: vm_config_memory
431 + title: Virtual Machine configured memory
432 + context: vm_config_memory
433 + units: MiB
434 + algorithm: absolute
435 + type: line
436 + priority: 70079
437 + lifecycle:
438 + expire_after_cycles: 10
439 + dimensions:
440 + - selector: vm_config_memory_memory
441 + name: memory
442 + - id: vm_config_devices
443 + title: Virtual Machine configured devices
444 + context: vm_config_devices
445 + units: devices
446 + algorithm: absolute
447 + type: line
448 + priority: 70080
449 + lifecycle:
450 + expire_after_cycles: 10
451 + dimensions:
452 + - selector: vm_config_devices_disks
453 + name: disks
454 + - selector: vm_config_devices_nics
455 + name: nics
456 + - family: vms storage
457 + metrics:
458 + - vm_storage_usage_committed
459 + - vm_storage_usage_uncommitted
460 + - vm_storage_usage_unshared
461 + chart_defaults:
462 + instances:
463 + by_labels:
464 + - id
465 + charts:
466 + - id: vm_storage_usage
467 + title: Virtual Machine storage usage
468 + context: vm_storage_usage
469 + units: bytes
470 + algorithm: absolute
471 + type: line
472 + priority: 70081
473 + lifecycle:
474 + expire_after_cycles: 10
475 + dimensions:
476 + - selector: vm_storage_usage_committed
477 + name: committed
478 + - selector: vm_storage_usage_uncommitted
479 + name: uncommitted
480 + - selector: vm_storage_usage_unshared
481 + name: unshared
482 + - family: vms snapshots
483 + metrics:
484 + - vm_snapshot_count_count
485 + - vm_snapshot_max_age_age
486 + - vm_snapshot_max_chain_depth_depth
487 + chart_defaults:
488 + instances:
489 + by_labels:
490 + - id
491 + charts:
492 + - id: vm_snapshot_count
493 + title: Virtual Machine snapshot count
494 + context: vm_snapshot_count
495 + units: snapshots
496 + algorithm: absolute
497 + type: line
498 + priority: 70066
499 + lifecycle:
500 + expire_after_cycles: 10
501 + dimensions:
502 + - selector: vm_snapshot_count_count
503 + name: count
504 + - id: vm_snapshot_max_age
505 + title: Virtual Machine oldest snapshot age
506 + context: vm_snapshot_max_age
507 + units: seconds
508 + algorithm: absolute
509 + type: line
510 + priority: 70067
511 + lifecycle:
512 + expire_after_cycles: 10
513 + dimensions:
514 + - selector: vm_snapshot_max_age_age
515 + name: age
516 + - id: vm_snapshot_max_chain_depth
517 + title: Virtual Machine maximum snapshot chain depth
518 + context: vm_snapshot_max_chain_depth
519 + units: snapshots
520 + algorithm: absolute
521 + type: line
522 + priority: 70068
523 + lifecycle:
524 + expire_after_cycles: 10
525 + dimensions:
526 + - selector: vm_snapshot_max_chain_depth_depth
527 + name: depth
528 + - family: hosts cpu
529 + metrics:
530 + - host_cpu_utilization_used
531 + chart_defaults:
532 + instances:
533 + by_labels:
534 + - id
535 + charts:
536 + - id: host_cpu_utilization
537 + title: ESXi Host CPU utilization
538 + context: host_cpu_utilization
539 + units: percentage
540 + algorithm: absolute
541 + type: line
542 + priority: 70054
543 + lifecycle:
544 + expire_after_cycles: 10
545 + dimensions:
546 + - selector: host_cpu_utilization_used
547 + name: used
548 + options:
549 + divisor: 100
550 + - family: hosts mem
551 + metrics:
552 + - host_mem_utilization_used
553 + - host_mem_usage_granted
554 + - host_mem_usage_consumed
555 + - host_mem_usage_active
556 + - host_mem_usage_shared
557 + - host_mem_usage_sharedcommon
558 + - host_mem_swap_io_in
559 + - host_mem_swap_io_out
560 + chart_defaults:
561 + instances:
562 + by_labels:
563 + - id
564 + charts:
565 + - id: host_mem_utilization
566 + title: ESXi Host memory utilization
567 + context: host_mem_utilization
568 + units: percentage
569 + algorithm: absolute
570 + type: line
571 + priority: 70055
572 + lifecycle:
573 + expire_after_cycles: 10
574 + dimensions:
575 + - selector: host_mem_utilization_used
576 + name: used
577 + options:
578 + divisor: 100
579 + - id: host_mem_usage
580 + title: ESXi Host memory usage
581 + context: host_mem_usage
582 + units: KiB
583 + algorithm: absolute
584 + type: line
585 + priority: 70056
586 + lifecycle:
587 + expire_after_cycles: 10
588 + dimensions:
589 + - selector: host_mem_usage_granted
590 + name: granted
591 + - selector: host_mem_usage_consumed
592 + name: consumed
593 + - selector: host_mem_usage_active
594 + name: active
595 + - selector: host_mem_usage_shared
596 + name: shared
597 + - selector: host_mem_usage_sharedcommon
598 + name: sharedcommon
599 + - id: host_mem_swap_io
600 + title: ESXi Host VMKernel memory swap IO
601 + context: host_mem_swap_io
602 + units: KiB/s
603 + algorithm: absolute
604 + type: area
605 + priority: 70057
606 + lifecycle:
607 + expire_after_cycles: 10
608 + dimensions:
609 + - selector: host_mem_swap_io_in
610 + name: in
611 + - selector: host_mem_swap_io_out
612 + name: out
613 + - family: hosts disk
614 + metrics:
615 + - host_disk_io_read
616 + - host_disk_io_write
617 + - host_disk_max_latency_latency
618 + chart_defaults:
619 + instances:
620 + by_labels:
621 + - id
622 + charts:
623 + - id: host_disk_io
624 + title: ESXi Host disk IO
625 + context: host_disk_io
626 + units: KiB/s
627 + algorithm: absolute
628 + type: area
629 + priority: 70058
630 + lifecycle:
631 + expire_after_cycles: 10
632 + dimensions:
633 + - selector: host_disk_io_read
634 + name: read
635 + - selector: host_disk_io_write
636 + name: write
637 + options:
638 + multiplier: -1
639 + - id: host_disk_max_latency
640 + title: ESXi Host disk max latency
641 + context: host_disk_max_latency
642 + units: milliseconds
643 + algorithm: absolute
644 + type: line
645 + priority: 70059
646 + lifecycle:
647 + expire_after_cycles: 10
648 + dimensions:
649 + - selector: host_disk_max_latency_latency
650 + name: latency
651 + - family: hosts net
652 + metrics:
653 + - host_net_traffic_received
654 + - host_net_traffic_sent
655 + - host_net_packets_received
656 + - host_net_packets_sent
657 + - host_net_drops_received
658 + - host_net_drops_sent
659 + - host_net_errors_received
660 + - host_net_errors_sent
661 + chart_defaults:
662 + instances:
663 + by_labels:
664 + - id
665 + charts:
666 + - id: host_net_traffic
667 + title: ESXi Host network traffic
668 + context: host_net_traffic
669 + units: KiB/s
670 + algorithm: absolute
671 + type: area
672 + priority: 70060
673 + lifecycle:
674 + expire_after_cycles: 10
675 + dimensions:
676 + - selector: host_net_traffic_received
677 + name: received
678 + - selector: host_net_traffic_sent
679 + name: sent
680 + options:
681 + multiplier: -1
682 + - id: host_net_packets
683 + title: ESXi Host network packets
684 + context: host_net_packets
685 + units: packets
686 + algorithm: absolute
687 + type: line
688 + priority: 70061
689 + lifecycle:
690 + expire_after_cycles: 10
691 + dimensions:
692 + - selector: host_net_packets_received
693 + name: received
694 + - selector: host_net_packets_sent
695 + name: sent
696 + options:
697 + multiplier: -1
698 + - id: host_net_drops
699 + title: ESXi Host network drops
700 + context: host_net_drops
701 + units: drops
702 + algorithm: absolute
703 + type: line
704 + priority: 70062
705 + lifecycle:
706 + expire_after_cycles: 10
707 + dimensions:
708 + - selector: host_net_drops_received
709 + name: received
710 + - selector: host_net_drops_sent
711 + name: sent
712 + options:
713 + multiplier: -1
714 + - id: host_net_errors
715 + title: ESXi Host network errors
716 + context: host_net_errors
717 + units: errors
718 + algorithm: absolute
719 + type: line
720 + priority: 70063
721 + lifecycle:
722 + expire_after_cycles: 10
723 + dimensions:
724 + - selector: host_net_errors_received
725 + name: received
726 + - selector: host_net_errors_sent
727 + name: sent
728 + options:
729 + multiplier: -1
730 + - family: hosts status
731 + metrics:
732 + - host_overall_status_green
733 + - host_overall_status_red
734 + - host_overall_status_yellow
735 + - host_overall_status_gray
736 + - host_power_state_powered_on
737 + - host_power_state_powered_off
738 + - host_power_state_standby
739 + - host_power_state_unknown
740 + - host_connection_state_connected
741 + - host_connection_state_not_responding
742 + - host_connection_state_disconnected
743 + - host_maintenance_status_normal
744 + - host_maintenance_status_in_maintenance
745 + chart_defaults:
746 + instances:
747 + by_labels:
748 + - id
749 + charts:
750 + - id: host_overall_status
751 + title: ESXi Host overall alarm status
752 + context: host_overall_status
753 + units: status
754 + algorithm: absolute
755 + type: line
756 + priority: 70064
757 + lifecycle:
758 + expire_after_cycles: 10
759 + dimensions:
760 + - selector: host_overall_status_green
761 + name: green
762 + - selector: host_overall_status_red
763 + name: red
764 + - selector: host_overall_status_yellow
765 + name: yellow
766 + - selector: host_overall_status_gray
767 + name: gray
768 + - id: host_power_state
769 + title: ESXi Host power state
770 + context: host_power_state
771 + units: status
772 + algorithm: absolute
773 + type: line
774 + priority: 70073
775 + lifecycle:
776 + expire_after_cycles: 10
777 + dimensions:
778 + - selector: host_power_state_powered_on
779 + name: powered_on
780 + - selector: host_power_state_powered_off
781 + name: powered_off
782 + - selector: host_power_state_standby
783 + name: standby
784 + - selector: host_power_state_unknown
785 + name: unknown
786 + - id: host_connection_state
787 + title: ESXi Host connection state
788 + context: host_connection_state
789 + units: status
790 + algorithm: absolute
791 + type: line
792 + priority: 70082
793 + lifecycle:
794 + expire_after_cycles: 10
795 + dimensions:
796 + - selector: host_connection_state_connected
797 + name: connected
798 + - selector: host_connection_state_not_responding
799 + name: not_responding
800 + - selector: host_connection_state_disconnected
801 + name: disconnected
802 + - id: host_maintenance_status
803 + title: ESXi Host maintenance status
804 + context: host_maintenance_status
805 + units: status
806 + algorithm: absolute
807 + type: line
808 + priority: 70083
809 + lifecycle:
810 + expire_after_cycles: 10
811 + dimensions:
812 + - selector: host_maintenance_status_normal
813 + name: normal
814 + - selector: host_maintenance_status_in_maintenance
815 + name: in_maintenance
816 + - family: hosts uptime
817 + metrics:
818 + - host_system_uptime_uptime
819 + chart_defaults:
820 + instances:
821 + by_labels:
822 + - id
823 + charts:
824 + - id: host_system_uptime
825 + title: ESXi Host system uptime
826 + context: host_system_uptime
827 + units: seconds
828 + algorithm: absolute
829 + type: line
830 + priority: 70065
831 + lifecycle:
832 + expire_after_cycles: 10
833 + dimensions:
834 + - selector: host_system_uptime_uptime
835 + name: uptime
836 + - family: datastores space
837 + metrics:
838 + - datastore_space_utilization_used
839 + - datastore_space_usage_capacity
840 + - datastore_space_usage_free
841 + - datastore_space_usage_used
842 + - datastore_space_usage_uncommitted
843 + chart_defaults:
844 + instances:
845 + by_labels:
846 + - id
847 + charts:
848 + - id: datastore_space_utilization
849 + title: Datastore space utilization
850 + context: datastore_space_utilization
851 + units: percentage
852 + algorithm: absolute
853 + type: line
854 + priority: 70039
855 + lifecycle:
856 + expire_after_cycles: 10
857 + dimensions:
858 + - selector: datastore_space_utilization_used
859 + name: used
860 + options:
861 + divisor: 100
862 + - id: datastore_space_usage
863 + title: Datastore space usage
864 + context: datastore_space_usage
865 + units: bytes
866 + algorithm: absolute
867 + type: line
868 + priority: 70040
869 + lifecycle:
870 + expire_after_cycles: 10
871 + dimensions:
872 + - selector: datastore_space_usage_capacity
873 + name: capacity
874 + - selector: datastore_space_usage_free
875 + name: free
876 + - selector: datastore_space_usage_used
877 + name: used
878 + - selector: datastore_space_usage_uncommitted
879 + name: uncommitted
880 + - family: datastores status
881 + metrics:
882 + - datastore_overall_status_green
883 + - datastore_overall_status_red
884 + - datastore_overall_status_yellow
885 + - datastore_overall_status_gray
886 + - datastore_accessibility_status_accessible
887 + - datastore_accessibility_status_inaccessible
888 + - datastore_maintenance_status_normal
889 + - datastore_maintenance_status_entering_maintenance
890 + - datastore_maintenance_status_in_maintenance
891 + - datastore_maintenance_status_unknown
892 + - datastore_multiple_host_access_enabled
893 + - datastore_multiple_host_access_disabled
894 + - datastore_multiple_host_access_unknown
895 + chart_defaults:
896 + instances:
897 + by_labels:
898 + - id
899 + charts:
900 + - id: datastore_overall_status
901 + title: Datastore overall alarm status
902 + context: datastore_overall_status
903 + units: status
904 + algorithm: absolute
905 + type: line
906 + priority: 70041
907 + lifecycle:
908 + expire_after_cycles: 10
909 + dimensions:
910 + - selector: datastore_overall_status_green
911 + name: green
912 + - selector: datastore_overall_status_red
913 + name: red
914 + - selector: datastore_overall_status_yellow
915 + name: yellow
916 + - selector: datastore_overall_status_gray
917 + name: gray
918 + - id: datastore_accessibility_status
919 + title: Datastore accessibility status
920 + context: datastore_accessibility_status
921 + units: status
922 + algorithm: absolute
923 + type: line
924 + priority: 70069
925 + lifecycle:
926 + expire_after_cycles: 10
927 + dimensions:
928 + - selector: datastore_accessibility_status_accessible
929 + name: accessible
930 + - selector: datastore_accessibility_status_inaccessible
931 + name: inaccessible
932 + - id: datastore_maintenance_status
933 + title: Datastore maintenance mode status
934 + context: datastore_maintenance_status
935 + units: status
936 + algorithm: absolute
937 + type: line
938 + priority: 70070
939 + lifecycle:
940 + expire_after_cycles: 10
941 + dimensions:
942 + - selector: datastore_maintenance_status_normal
943 + name: normal
944 + - selector: datastore_maintenance_status_entering_maintenance
945 + name: entering_maintenance
946 + - selector: datastore_maintenance_status_in_maintenance
947 + name: in_maintenance
948 + - selector: datastore_maintenance_status_unknown
949 + name: unknown
950 + - id: datastore_multiple_host_access
951 + title: Datastore multi-host access status
952 + context: datastore_multiple_host_access
953 + units: status
954 + algorithm: absolute
955 + type: line
956 + priority: 70071
957 + lifecycle:
958 + expire_after_cycles: 10
959 + dimensions:
960 + - selector: datastore_multiple_host_access_enabled
961 + name: enabled
962 + - selector: datastore_multiple_host_access_disabled
963 + name: disabled
964 + - selector: datastore_multiple_host_access_unknown
965 + name: unknown
966 + - family: datastores disk
967 + metrics:
968 + - datastore_disk_io_read
969 + - datastore_disk_io_write
970 + - datastore_disk_iops_reads
971 + - datastore_disk_iops_writes
972 + - datastore_disk_latency_read
973 + - datastore_disk_latency_write
974 + chart_defaults:
975 + instances:
976 + by_labels:
977 + - id
978 + charts:
979 + - id: datastore_disk_io
980 + title: Datastore disk IO
981 + context: datastore_disk_io
982 + units: KiB/s
983 + algorithm: absolute
984 + type: area
985 + priority: 70036
986 + lifecycle:
987 + expire_after_cycles: 10
988 + dimensions:
989 + - selector: datastore_disk_io_read
990 + name: read
991 + - selector: datastore_disk_io_write
992 + name: write
993 + options:
994 + multiplier: -1
995 + - id: datastore_disk_iops
996 + title: Datastore disk IOPS
997 + context: datastore_disk_iops
998 + units: operations/s
999 + algorithm: absolute
1000 + type: line
1001 + priority: 70037
1002 + lifecycle:
1003 + expire_after_cycles: 10
1004 + dimensions:
1005 + - selector: datastore_disk_iops_reads
1006 + name: reads
1007 + - selector: datastore_disk_iops_writes
1008 + name: writes
1009 + options:
1010 + multiplier: -1
1011 + - id: datastore_disk_latency
1012 + title: Datastore disk latency
1013 + context: datastore_disk_latency
1014 + units: milliseconds
1015 + algorithm: absolute
1016 + type: line
1017 + priority: 70038
1018 + lifecycle:
1019 + expire_after_cycles: 10
1020 + dimensions:
1021 + - selector: datastore_disk_latency_read
1022 + name: read
1023 + - selector: datastore_disk_latency_write
1024 + name: write
1025 + - family: clusters hosts
1026 + metrics:
1027 + - cluster_hosts_total
1028 + - cluster_hosts_effective
1029 + chart_defaults:
1030 + instances:
1031 + by_labels:
1032 + - id
1033 + charts:
1034 + - id: cluster_hosts
1035 + title: Cluster host count
1036 + context: cluster_hosts
1037 + units: hosts
1038 + algorithm: absolute
1039 + type: line
1040 + priority: 70000
1041 + lifecycle:
1042 + expire_after_cycles: 10
1043 + dimensions:
1044 + - selector: cluster_hosts_total
1045 + name: total
1046 + - selector: cluster_hosts_effective
1047 + name: effective
1048 + - family: clusters cpu
1049 + metrics:
1050 + - cluster_cpu_capacity_total
1051 + - cluster_cpu_capacity_effective
1052 + - cluster_cpu_topology_cores
1053 + - cluster_cpu_topology_threads
1054 + - cluster_usage_cpu_demand
1055 + - cluster_usage_cpu_entitled
1056 + - cluster_usage_cpu_reserved
1057 + chart_defaults:
1058 + instances:
1059 + by_labels:
1060 + - id
1061 + charts:
1062 + - id: cluster_cpu_capacity
1063 + title: Cluster CPU capacity
1064 + context: cluster_cpu_capacity
1065 + units: MHz
1066 + algorithm: absolute
1067 + type: line
1068 + priority: 70001
1069 + lifecycle:
1070 + expire_after_cycles: 10
1071 + dimensions:
1072 + - selector: cluster_cpu_capacity_total
1073 + name: total
1074 + - selector: cluster_cpu_capacity_effective
1075 + name: effective
1076 + - id: cluster_cpu_topology
1077 + title: Cluster CPU topology
1078 + context: cluster_cpu_topology
1079 + units: count
1080 + algorithm: absolute
1081 + type: line
1082 + priority: 70003
1083 + lifecycle:
1084 + expire_after_cycles: 10
1085 + dimensions:
1086 + - selector: cluster_cpu_topology_cores
1087 + name: cores
1088 + - selector: cluster_cpu_topology_threads
1089 + name: threads
1090 + - id: cluster_usage_cpu
1091 + title: Cluster DRS CPU usage summary
1092 + context: cluster_usage_cpu
1093 + units: MHz
1094 + algorithm: absolute
1095 + type: line
1096 + priority: 70011
1097 + lifecycle:
1098 + expire_after_cycles: 10
1099 + dimensions:
1100 + - selector: cluster_usage_cpu_demand
1101 + name: demand
1102 + - selector: cluster_usage_cpu_entitled
1103 + name: entitled
1104 + - selector: cluster_usage_cpu_reserved
1105 + name: reserved
1106 + - family: clusters mem
1107 + metrics:
1108 + - cluster_mem_capacity_total
1109 + - cluster_mem_capacity_effective
1110 + - cluster_usage_mem_demand
1111 + - cluster_usage_mem_entitled
1112 + - cluster_usage_mem_reserved
1113 + chart_defaults:
1114 + instances:
1115 + by_labels:
1116 + - id
1117 + charts:
1118 + - id: cluster_mem_capacity
1119 + title: Cluster memory capacity
1120 + context: cluster_mem_capacity
1121 + units: bytes
1122 + algorithm: absolute
1123 + type: line
1124 + priority: 70002
1125 + lifecycle:
1126 + expire_after_cycles: 10
1127 + dimensions:
1128 + - selector: cluster_mem_capacity_total
1129 + name: total
1130 + - selector: cluster_mem_capacity_effective
1131 + name: effective
1132 + - id: cluster_usage_mem
1133 + title: Cluster DRS memory usage summary
1134 + context: cluster_usage_mem
1135 + units: MB
1136 + algorithm: absolute
1137 + type: line
1138 + priority: 70012
1139 + lifecycle:
1140 + expire_after_cycles: 10
1141 + dimensions:
1142 + - selector: cluster_usage_mem_demand
1143 + name: demand
1144 + - selector: cluster_usage_mem_entitled
1145 + name: entitled
1146 + - selector: cluster_usage_mem_reserved
1147 + name: reserved
1148 + - family: clusters config
1149 + metrics:
1150 + - cluster_drs_config_enabled
1151 + - cluster_drs_mode_manual
1152 + - cluster_drs_mode_partially_automated
1153 + - cluster_drs_mode_fully_automated
1154 + - cluster_drs_mode_unknown
1155 + - cluster_drs_vmotion_rate_rate
1156 + - cluster_ha_config_enabled
1157 + - cluster_ha_config_admission_control
1158 + - cluster_ha_host_monitoring_enabled
1159 + - cluster_ha_host_monitoring_disabled
1160 + - cluster_ha_host_monitoring_unknown
1161 + - cluster_ha_vm_monitoring_disabled
1162 + - cluster_ha_vm_monitoring_vm_monitoring_only
1163 + - cluster_ha_vm_monitoring_vm_and_app_monitoring
1164 + - cluster_ha_vm_monitoring_unknown
1165 + - cluster_ha_vm_component_protection_enabled
1166 + - cluster_ha_vm_component_protection_disabled
1167 + - cluster_ha_vm_component_protection_unknown
1168 + chart_defaults:
1169 + instances:
1170 + by_labels:
1171 + - id
1172 + charts:
1173 + - id: cluster_drs_config
1174 + title: Cluster DRS enabled
1175 + context: cluster_drs_config
1176 + units: status
1177 + algorithm: absolute
1178 + type: line
1179 + priority: 70004
1180 + lifecycle:
1181 + expire_after_cycles: 10
1182 + dimensions:
1183 + - selector: cluster_drs_config_enabled
1184 + name: enabled
1185 + - id: cluster_drs_mode
1186 + title: Cluster DRS automation mode
1187 + context: cluster_drs_mode
1188 + units: status
1189 + algorithm: absolute
1190 + type: line
1191 + priority: 70084
1192 + lifecycle:
1193 + expire_after_cycles: 10
1194 + dimensions:
1195 + - selector: cluster_drs_mode_manual
1196 + name: manual
1197 + - selector: cluster_drs_mode_partially_automated
1198 + name: partially_automated
1199 + - selector: cluster_drs_mode_fully_automated
1200 + name: fully_automated
1201 + - selector: cluster_drs_mode_unknown
1202 + name: unknown
1203 + - id: cluster_drs_vmotion_rate
1204 + title: Cluster DRS vMotion recommendation threshold
1205 + context: cluster_drs_vmotion_rate
1206 + units: level
1207 + algorithm: absolute
1208 + type: line
1209 + priority: 70085
1210 + lifecycle:
1211 + expire_after_cycles: 10
1212 + dimensions:
1213 + - selector: cluster_drs_vmotion_rate_rate
1214 + name: rate
1215 + - id: cluster_ha_config
1216 + title: Cluster HA configuration
1217 + context: cluster_ha_config
1218 + units: status
1219 + algorithm: absolute
1220 + type: line
1221 + priority: 70005
1222 + lifecycle:
1223 + expire_after_cycles: 10
1224 + dimensions:
1225 + - selector: cluster_ha_config_enabled
1226 + name: enabled
1227 + - selector: cluster_ha_config_admission_control
1228 + name: admission_control
1229 + - id: cluster_ha_host_monitoring
1230 + title: Cluster HA host monitoring
1231 + context: cluster_ha_host_monitoring
1232 + units: status
1233 + algorithm: absolute
1234 + type: line
1235 + priority: 70086
1236 + lifecycle:
1237 + expire_after_cycles: 10
1238 + dimensions:
1239 + - selector: cluster_ha_host_monitoring_enabled
1240 + name: enabled
1241 + - selector: cluster_ha_host_monitoring_disabled
1242 + name: disabled
1243 + - selector: cluster_ha_host_monitoring_unknown
1244 + name: unknown
1245 + - id: cluster_ha_vm_monitoring
1246 + title: Cluster HA VM monitoring
1247 + context: cluster_ha_vm_monitoring
1248 + units: status
1249 + algorithm: absolute
1250 + type: line
1251 + priority: 70087
1252 + lifecycle:
1253 + expire_after_cycles: 10
1254 + dimensions:
1255 + - selector: cluster_ha_vm_monitoring_disabled
1256 + name: disabled
1257 + - selector: cluster_ha_vm_monitoring_vm_monitoring_only
1258 + name: vm_monitoring_only
1259 + - selector: cluster_ha_vm_monitoring_vm_and_app_monitoring
1260 + name: vm_and_app_monitoring
1261 + - selector: cluster_ha_vm_monitoring_unknown
1262 + name: unknown
1263 + - id: cluster_ha_vm_component_protection
1264 + title: Cluster HA VM component protection
1265 + context: cluster_ha_vm_component_protection
1266 + units: status
1267 + algorithm: absolute
1268 + type: line
1269 + priority: 70088
1270 + lifecycle:
1271 + expire_after_cycles: 10
1272 + dimensions:
1273 + - selector: cluster_ha_vm_component_protection_enabled
1274 + name: enabled
1275 + - selector: cluster_ha_vm_component_protection_disabled
1276 + name: disabled
1277 + - selector: cluster_ha_vm_component_protection_unknown
1278 + name: unknown
1279 + - family: clusters status
1280 + metrics:
1281 + - cluster_overall_status_green
1282 + - cluster_overall_status_red
1283 + - cluster_overall_status_yellow
1284 + - cluster_overall_status_gray
1285 + chart_defaults:
1286 + instances:
1287 + by_labels:
1288 + - id
1289 + charts:
1290 + - id: cluster_overall_status
1291 + title: Cluster overall alarm status
1292 + context: cluster_overall_status
1293 + units: status
1294 + algorithm: absolute
1295 + type: line
1296 + priority: 70006
1297 + lifecycle:
1298 + expire_after_cycles: 10
1299 + dimensions:
1300 + - selector: cluster_overall_status_green
1301 + name: green
1302 + - selector: cluster_overall_status_red
1303 + name: red
1304 + - selector: cluster_overall_status_yellow
1305 + name: yellow
1306 + - selector: cluster_overall_status_gray
1307 + name: gray
1308 + - family: clusters migrations
1309 + metrics:
1310 + - cluster_vmotions_vmotions
1311 + chart_defaults:
1312 + instances:
1313 + by_labels:
1314 + - id
1315 + charts:
1316 + - id: cluster_vmotions
1317 + title: Cluster cumulative vMotion count
1318 + context: cluster_vmotions
1319 + units: migrations
1320 + algorithm: incremental
1321 + type: line
1322 + priority: 70007
1323 + lifecycle:
1324 + expire_after_cycles: 10
1325 + dimensions:
1326 + - selector: cluster_vmotions_vmotions
1327 + name: vmotions
1328 + - family: clusters drs
1329 + metrics:
1330 + - cluster_drs_score_score
1331 + - cluster_drs_balance_current
1332 + - cluster_drs_balance_target
1333 + chart_defaults:
1334 + instances:
1335 + by_labels:
1336 + - id
1337 + charts:
1338 + - id: cluster_drs_score
1339 + title: Cluster DRS score
1340 + context: cluster_drs_score
1341 + units: percentage
1342 + algorithm: absolute
1343 + type: line
1344 + priority: 70008
1345 + lifecycle:
1346 + expire_after_cycles: 10
1347 + dimensions:
1348 + - selector: cluster_drs_score_score
1349 + name: score
1350 + - id: cluster_drs_balance
1351 + title: Cluster DRS load balance
1352 + context: cluster_drs_balance
1353 + units: score
1354 + algorithm: absolute
1355 + type: line
1356 + priority: 70009
1357 + lifecycle:
1358 + expire_after_cycles: 10
1359 + dimensions:
1360 + - selector: cluster_drs_balance_current
1361 + name: current
1362 + options:
1363 + divisor: 1000
1364 + - selector: cluster_drs_balance_target
1365 + name: target
1366 + options:
1367 + divisor: 1000
1368 + - family: clusters vms
1369 + metrics:
1370 + - cluster_vm_count_total
1371 + - cluster_vm_count_powered_off
1372 + chart_defaults:
1373 + instances:
1374 + by_labels:
1375 + - id
1376 + charts:
1377 + - id: cluster_vm_count
1378 + title: Cluster VM count
1379 + context: cluster_vm_count
1380 + units: VMs
1381 + algorithm: absolute
1382 + type: line
1383 + priority: 70010
1384 + lifecycle:
1385 + expire_after_cycles: 10
1386 + dimensions:
1387 + - selector: cluster_vm_count_total
1388 + name: total
1389 + - selector: cluster_vm_count_powered_off
1390 + name: powered_off
1391 + - family: clusters cpu
1392 + metrics:
1393 + - cluster_cpu_utilization_used
1394 + - cluster_cpu_usage_used
1395 + - cluster_cpu_usage_total
1396 + - cluster_services_effective_cpu_effective_cpu
1397 + chart_defaults:
1398 + instances:
1399 + by_labels:
1400 + - id
1401 + charts:
1402 + - id: cluster_cpu_utilization
1403 + title: Cluster CPU utilization
1404 + context: cluster_cpu_utilization
1405 + units: percentage
1406 + algorithm: absolute
1407 + type: line
1408 + priority: 70013
1409 + lifecycle:
1410 + expire_after_cycles: 10
1411 + dimensions:
1412 + - selector: cluster_cpu_utilization_used
1413 + name: used
1414 + options:
1415 + divisor: 100
1416 + - id: cluster_cpu_usage
1417 + title: Cluster CPU usage
1418 + context: cluster_cpu_usage
1419 + units: MHz
1420 + algorithm: absolute
1421 + type: line
1422 + priority: 70014
1423 + lifecycle:
1424 + expire_after_cycles: 10
1425 + dimensions:
1426 + - selector: cluster_cpu_usage_used
1427 + name: used
1428 + - selector: cluster_cpu_usage_total
1429 + name: total
1430 + - id: cluster_services_effective_cpu
1431 + title: Cluster effective CPU capacity
1432 + context: cluster_services_effective_cpu
1433 + units: MHz
1434 + algorithm: absolute
1435 + type: line
1436 + priority: 70018
1437 + lifecycle:
1438 + expire_after_cycles: 10
1439 + dimensions:
1440 + - selector: cluster_services_effective_cpu_effective_cpu
1441 + name: effective_cpu
1442 + - family: clusters mem
1443 + metrics:
1444 + - cluster_mem_utilization_used
1445 + - cluster_mem_usage_consumed
1446 + - cluster_mem_usage_active
1447 + - cluster_mem_usage_granted
1448 + - cluster_mem_usage_shared
1449 + - cluster_mem_usage_overhead
1450 + - cluster_mem_usage_swap_used
1451 + - cluster_services_effective_mem_effective_mem
1452 + chart_defaults:
1453 + instances:
1454 + by_labels:
1455 + - id
1456 + charts:
1457 + - id: cluster_mem_utilization
1458 + title: Cluster memory utilization
1459 + context: cluster_mem_utilization
1460 + units: percentage
1461 + algorithm: absolute
1462 + type: line
1463 + priority: 70015
1464 + lifecycle:
1465 + expire_after_cycles: 10
1466 + dimensions:
1467 + - selector: cluster_mem_utilization_used
1468 + name: used
1469 + options:
1470 + divisor: 100
1471 + - id: cluster_mem_usage
1472 + title: Cluster memory usage
1473 + context: cluster_mem_usage
1474 + units: KiB
1475 + algorithm: absolute
1476 + type: line
1477 + priority: 70016
1478 + lifecycle:
1479 + expire_after_cycles: 10
1480 + dimensions:
1481 + - selector: cluster_mem_usage_consumed
1482 + name: consumed
1483 + - selector: cluster_mem_usage_active
1484 + name: active
1485 + - selector: cluster_mem_usage_granted
1486 + name: granted
1487 + - selector: cluster_mem_usage_shared
1488 + name: shared
1489 + - selector: cluster_mem_usage_overhead
1490 + name: overhead
1491 + - selector: cluster_mem_usage_swap_used
1492 + name: swap_used
1493 + - id: cluster_services_effective_mem
1494 + title: Cluster effective memory capacity
1495 + context: cluster_services_effective_mem
1496 + units: MB
1497 + algorithm: absolute
1498 + type: line
1499 + priority: 70019
1500 + lifecycle:
1501 + expire_after_cycles: 10
1502 + dimensions:
1503 + - selector: cluster_services_effective_mem_effective_mem
1504 + name: effective_mem
1505 + - family: clusters drs
1506 + metrics:
1507 + - cluster_services_fairness_cpu
1508 + - cluster_services_fairness_memory
1509 + chart_defaults:
1510 + instances:
1511 + by_labels:
1512 + - id
1513 + charts:
1514 + - id: cluster_services_fairness
1515 + title: Cluster DRS resource distribution fairness
1516 + context: cluster_services_fairness
1517 + units: score
1518 + algorithm: absolute
1519 + type: line
1520 + priority: 70017
1521 + lifecycle:
1522 + expire_after_cycles: 10
1523 + dimensions:
1524 + - selector: cluster_services_fairness_cpu
1525 + name: cpu
1526 + - selector: cluster_services_fairness_memory
1527 + name: memory
1528 + - family: clusters ha
1529 + metrics:
1530 + - cluster_services_failover_failures_tolerable
1531 + chart_defaults:
1532 + instances:
1533 + by_labels:
1534 + - id
1535 + charts:
1536 + - id: cluster_services_failover
1537 + title: Cluster HA failover capacity
1538 + context: cluster_services_failover
1539 + units: failures
1540 + algorithm: absolute
1541 + type: line
1542 + priority: 70020
1543 + lifecycle:
1544 + expire_after_cycles: 10
1545 + dimensions:
1546 + - selector: cluster_services_failover_failures_tolerable
1547 + name: failures_tolerable
1548 + - family: clusters vmop
1549 + metrics:
1550 + - cluster_vm_migrations_vmotion
1551 + - cluster_vm_migrations_svmotion
1552 + - cluster_vm_migrations_xvmotion
1553 + - cluster_vm_lifecycle_poweron
1554 + - cluster_vm_lifecycle_poweroff
1555 + - cluster_vm_lifecycle_create
1556 + - cluster_vm_lifecycle_destroy
1557 + - cluster_vm_lifecycle_clone
1558 + - cluster_vm_lifecycle_deploy
1559 + - cluster_vm_management_reconfigure
1560 + - cluster_vm_management_reset
1561 + - cluster_vm_management_suspend
1562 + - cluster_vm_management_register
1563 + - cluster_vm_management_unregister
1564 + - cluster_vm_guest_ops_reboot
1565 + - cluster_vm_guest_ops_shutdown
1566 + - cluster_vm_guest_ops_standby
1567 + - cluster_vm_cold_migrations_change_ds
1568 + - cluster_vm_cold_migrations_change_host
1569 + - cluster_vm_cold_migrations_change_host_ds
1570 + chart_defaults:
1571 + instances:
1572 + by_labels:
1573 + - id
1574 + charts:
1575 + - id: cluster_vm_migrations
1576 + title: Cluster VM migration operations
1577 + context: cluster_vm_migrations
1578 + units: operations
1579 + algorithm: absolute
1580 + type: line
1581 + priority: 70021
1582 + lifecycle:
1583 + expire_after_cycles: 10
1584 + dimensions:
1585 + - selector: cluster_vm_migrations_vmotion
1586 + name: vmotion
1587 + - selector: cluster_vm_migrations_svmotion
1588 + name: svmotion
1589 + - selector: cluster_vm_migrations_xvmotion
1590 + name: xvmotion
1591 + - id: cluster_vm_lifecycle
1592 + title: Cluster VM lifecycle operations
1593 + context: cluster_vm_lifecycle
1594 + units: operations
1595 + algorithm: absolute
1596 + type: line
1597 + priority: 70022
1598 + lifecycle:
1599 + expire_after_cycles: 10
1600 + dimensions:
1601 + - selector: cluster_vm_lifecycle_poweron
1602 + name: poweron
1603 + - selector: cluster_vm_lifecycle_poweroff
1604 + name: poweroff
1605 + - selector: cluster_vm_lifecycle_create
1606 + name: create
1607 + - selector: cluster_vm_lifecycle_destroy
1608 + name: destroy
1609 + - selector: cluster_vm_lifecycle_clone
1610 + name: clone
1611 + - selector: cluster_vm_lifecycle_deploy
1612 + name: deploy
1613 + - id: cluster_vm_management
1614 + title: Cluster VM management operations
1615 + context: cluster_vm_management
1616 + units: operations
1617 + algorithm: absolute
1618 + type: line
1619 + priority: 70023
1620 + lifecycle:
1621 + expire_after_cycles: 10
1622 + dimensions:
1623 + - selector: cluster_vm_management_reconfigure
1624 + name: reconfigure
1625 + - selector: cluster_vm_management_reset
1626 + name: reset
1627 + - selector: cluster_vm_management_suspend
1628 + name: suspend
1629 + - selector: cluster_vm_management_register
1630 + name: register
1631 + - selector: cluster_vm_management_unregister
1632 + name: unregister
1633 + - id: cluster_vm_guest_ops
1634 + title: Cluster VM guest operations
1635 + context: cluster_vm_guest_ops
1636 + units: operations
1637 + algorithm: absolute
1638 + type: line
1639 + priority: 70024
1640 + lifecycle:
1641 + expire_after_cycles: 10
1642 + dimensions:
1643 + - selector: cluster_vm_guest_ops_reboot
1644 + name: reboot
1645 + - selector: cluster_vm_guest_ops_shutdown
1646 + name: shutdown
1647 + - selector: cluster_vm_guest_ops_standby
1648 + name: standby
1649 + - id: cluster_vm_cold_migrations
1650 + title: Cluster VM cold migration operations
1651 + context: cluster_vm_cold_migrations
1652 + units: operations
1653 + algorithm: absolute
1654 + type: line
1655 + priority: 70025
1656 + lifecycle:
1657 + expire_after_cycles: 10
1658 + dimensions:
1659 + - selector: cluster_vm_cold_migrations_change_ds
1660 + name: change_ds
1661 + - selector: cluster_vm_cold_migrations_change_host
1662 + name: change_host
1663 + - selector: cluster_vm_cold_migrations_change_host_ds
1664 + name: change_host_ds
1665 + - family: resource pools cpu
1666 + metrics:
1667 + - resource_pool_cpu_usage_usage
1668 + - resource_pool_cpu_usage_demand
1669 + - resource_pool_cpu_entitlement_distributed
1670 + - resource_pool_cpu_allocation_reservation_used
1671 + - resource_pool_cpu_allocation_unreserved_for_vm
1672 + - resource_pool_cpu_allocation_max_usage
1673 + - resource_pool_cpu_config_reservation
1674 + - resource_pool_cpu_config_limit
1675 + chart_defaults:
1676 + instances:
1677 + by_labels:
1678 + - id
1679 + charts:
1680 + - id: resource_pool_cpu_usage
1681 + title: Resource Pool CPU usage vs demand
1682 + context: resource_pool_cpu_usage
1683 + units: MHz
1684 + algorithm: absolute
1685 + type: line
1686 + priority: 70026
1687 + lifecycle:
1688 + expire_after_cycles: 10
1689 + dimensions:
1690 + - selector: resource_pool_cpu_usage_usage
1691 + name: usage
1692 + - selector: resource_pool_cpu_usage_demand
1693 + name: demand
1694 + - id: resource_pool_cpu_entitlement
1695 + title: Resource Pool CPU entitlement
1696 + context: resource_pool_cpu_entitlement
1697 + units: MHz
1698 + algorithm: absolute
1699 + type: line
1700 + priority: 70027
1701 + lifecycle:
1702 + expire_after_cycles: 10
1703 + dimensions:
1704 + - selector: resource_pool_cpu_entitlement_distributed
1705 + name: distributed
1706 + - id: resource_pool_cpu_allocation
1707 + title: Resource Pool CPU allocation
1708 + context: resource_pool_cpu_allocation
1709 + units: MHz
1710 + algorithm: absolute
1711 + type: line
1712 + priority: 70028
1713 + lifecycle:
1714 + expire_after_cycles: 10
1715 + dimensions:
1716 + - selector: resource_pool_cpu_allocation_reservation_used
1717 + name: reservation_used
1718 + - selector: resource_pool_cpu_allocation_unreserved_for_vm
1719 + name: unreserved_for_vm
1720 + - selector: resource_pool_cpu_allocation_max_usage
1721 + name: max_usage
1722 + - id: resource_pool_cpu_config
1723 + title: Resource Pool CPU configured reservation and limit
1724 + context: resource_pool_cpu_config
1725 + units: MHz
1726 + algorithm: absolute
1727 + type: line
1728 + priority: 70033
1729 + lifecycle:
1730 + expire_after_cycles: 10
1731 + dimensions:
1732 + - selector: resource_pool_cpu_config_reservation
1733 + name: reservation
1734 + - selector: resource_pool_cpu_config_limit
1735 + name: limit
1736 + - family: resource pools mem
1737 + metrics:
1738 + - resource_pool_mem_usage_host
1739 + - resource_pool_mem_usage_guest
1740 + - resource_pool_mem_entitlement_distributed
1741 + - resource_pool_mem_allocation_reservation_used
1742 + - resource_pool_mem_allocation_unreserved_for_vm
1743 + - resource_pool_mem_allocation_max_usage
1744 + - resource_pool_mem_breakdown_private
1745 + - resource_pool_mem_breakdown_shared
1746 + - resource_pool_mem_breakdown_swapped
1747 + - resource_pool_mem_breakdown_ballooned
1748 + - resource_pool_mem_breakdown_overhead
1749 + - resource_pool_mem_breakdown_consumed_overhead
1750 + - resource_pool_mem_breakdown_compressed
1751 + - resource_pool_mem_config_reservation
1752 + - resource_pool_mem_config_limit
1753 + chart_defaults:
1754 + instances:
1755 + by_labels:
1756 + - id
1757 + charts:
1758 + - id: resource_pool_mem_usage
1759 + title: Resource Pool memory usage
1760 + context: resource_pool_mem_usage
1761 + units: MB
1762 + algorithm: absolute
1763 + type: line
1764 + priority: 70029
1765 + lifecycle:
1766 + expire_after_cycles: 10
1767 + dimensions:
1768 + - selector: resource_pool_mem_usage_host
1769 + name: host
1770 + - selector: resource_pool_mem_usage_guest
1771 + name: guest
1772 + - id: resource_pool_mem_entitlement
1773 + title: Resource Pool memory entitlement
1774 + context: resource_pool_mem_entitlement
1775 + units: MB
1776 + algorithm: absolute
1777 + type: line
1778 + priority: 70030
1779 + lifecycle:
1780 + expire_after_cycles: 10
1781 + dimensions:
1782 + - selector: resource_pool_mem_entitlement_distributed
1783 + name: distributed
1784 + - id: resource_pool_mem_allocation
1785 + title: Resource Pool memory allocation
1786 + context: resource_pool_mem_allocation
1787 + units: bytes
1788 + algorithm: absolute
1789 + type: line
1790 + priority: 70031
1791 + lifecycle:
1792 + expire_after_cycles: 10
1793 + dimensions:
1794 + - selector: resource_pool_mem_allocation_reservation_used
1795 + name: reservation_used
1796 + - selector: resource_pool_mem_allocation_unreserved_for_vm
1797 + name: unreserved_for_vm
1798 + - selector: resource_pool_mem_allocation_max_usage
1799 + name: max_usage
1800 + - id: resource_pool_mem_breakdown
1801 + title: Resource Pool memory state breakdown
1802 + context: resource_pool_mem_breakdown
1803 + units: MB
1804 + algorithm: absolute
1805 + type: line
1806 + priority: 70032
1807 + lifecycle:
1808 + expire_after_cycles: 10
1809 + dimensions:
1810 + - selector: resource_pool_mem_breakdown_private
1811 + name: private
1812 + - selector: resource_pool_mem_breakdown_shared
1813 + name: shared
1814 + - selector: resource_pool_mem_breakdown_swapped
1815 + name: swapped
1816 + - selector: resource_pool_mem_breakdown_ballooned
1817 + name: ballooned
1818 + - selector: resource_pool_mem_breakdown_overhead
1819 + name: overhead
1820 + - selector: resource_pool_mem_breakdown_consumed_overhead
1821 + name: consumed_overhead
1822 + - selector: resource_pool_mem_breakdown_compressed
1823 + name: compressed
1824 + options:
1825 + divisor: 1024
1826 + - id: resource_pool_mem_config
1827 + title: Resource Pool memory configured reservation and limit
1828 + context: resource_pool_mem_config
1829 + units: MB
1830 + algorithm: absolute
1831 + type: line
1832 + priority: 70034
1833 + lifecycle:
1834 + expire_after_cycles: 10
1835 + dimensions:
1836 + - selector: resource_pool_mem_config_reservation
1837 + name: reservation
1838 + - selector: resource_pool_mem_config_limit
1839 + name: limit
1840 + - family: resource pools status
1841 + metrics:
1842 + - resource_pool_overall_status_green
1843 + - resource_pool_overall_status_red
1844 + - resource_pool_overall_status_yellow
1845 + - resource_pool_overall_status_gray
1846 + chart_defaults:
1847 + instances:
1848 + by_labels:
1849 + - id
1850 + charts:
1851 + - id: resource_pool_overall_status
1852 + title: Resource Pool overall alarm status
1853 + context: resource_pool_overall_status
1854 + units: status
1855 + algorithm: absolute
1856 + type: line
1857 + priority: 70035
1858 + lifecycle:
1859 + expire_after_cycles: 10
1860 + dimensions:
1861 + - selector: resource_pool_overall_status_green
1862 + name: green
1863 + - selector: resource_pool_overall_status_red
1864 + name: red
1865 + - selector: resource_pool_overall_status_yellow
1866 + name: yellow
1867 + - selector: resource_pool_overall_status_gray
1868 + name: gray
1869 + - family: datastore clusters space
1870 + metrics:
1871 + - datastore_cluster_space_usage_capacity
1872 + - datastore_cluster_space_usage_free
1873 + - datastore_cluster_space_usage_used
1874 + - datastore_cluster_space_utilization_used
1875 + chart_defaults:
1876 + instances:
1877 + by_labels:
1878 + - id
1879 + charts:
1880 + - id: datastore_cluster_space_utilization
1881 + title: Datastore Cluster space utilization
1882 + context: datastore_cluster_space_utilization
1883 + units: percentage
1884 + algorithm: absolute
1885 + type: line
1886 + priority: 70200
1887 + lifecycle:
1888 + expire_after_cycles: 10
1889 + dimensions:
1890 + - selector: datastore_cluster_space_utilization_used
1891 + name: used
1892 + options:
1893 + divisor: 100
1894 + - id: datastore_cluster_space_usage
1895 + title: Datastore Cluster space usage
1896 + context: datastore_cluster_space_usage
1897 + units: bytes
1898 + algorithm: absolute
1899 + type: line
1900 + priority: 70201
1901 + lifecycle:
1902 + expire_after_cycles: 10
1903 + dimensions:
1904 + - selector: datastore_cluster_space_usage_capacity
1905 + name: capacity
1906 + - selector: datastore_cluster_space_usage_free
1907 + name: free
1908 + - selector: datastore_cluster_space_usage_used
1909 + name: used
1910 + - family: datastore clusters status
1911 + metrics:
1912 + - datastore_cluster_storage_drs_status_enabled
1913 + - datastore_cluster_storage_drs_status_disabled
1914 + - datastore_cluster_overall_status_green
1915 + - datastore_cluster_overall_status_red
1916 + - datastore_cluster_overall_status_yellow
1917 + - datastore_cluster_overall_status_gray
1918 + chart_defaults:
1919 + instances:
1920 + by_labels:
1921 + - id
1922 + charts:
1923 + - id: datastore_cluster_storage_drs_status
1924 + title: Datastore Cluster Storage DRS status
1925 + context: datastore_cluster_storage_drs_status
1926 + units: status
1927 + algorithm: absolute
1928 + type: line
1929 + priority: 70202
1930 + lifecycle:
1931 + expire_after_cycles: 10
1932 + dimensions:
1933 + - selector: datastore_cluster_storage_drs_status_enabled
1934 + name: enabled
1935 + - selector: datastore_cluster_storage_drs_status_disabled
1936 + name: disabled
1937 + - id: datastore_cluster_overall_status
1938 + title: Datastore Cluster overall status
1939 + context: datastore_cluster_overall_status
1940 + units: status
1941 + algorithm: absolute
1942 + type: line
1943 + priority: 70203
1944 + lifecycle:
1945 + expire_after_cycles: 10
1946 + dimensions:
1947 + - selector: datastore_cluster_overall_status_green
1948 + name: green
1949 + - selector: datastore_cluster_overall_status_red
1950 + name: red
1951 + - selector: datastore_cluster_overall_status_yellow
1952 + name: yellow
1953 + - selector: datastore_cluster_overall_status_gray
1954 + name: gray
1955 + - family: hosts power
1956 + metrics:
1957 + - host_power_usage_power
1958 + - host_power_usage_cap
1959 + - host_power_capacity_usage_used
1960 + - host_power_capacity_usage_usable
1961 + - host_power_capacity_usage_idle
1962 + - host_power_capacity_usage_system
1963 + - host_power_capacity_usage_vm
1964 + - host_power_capacity_utilization_used
1965 + - host_energy_usage_energy
1966 + chart_defaults:
1967 + instances:
1968 + by_labels:
1969 + - id
1970 + charts:
1971 + - id: host_power_usage
1972 + title: ESXi Host power usage
1973 + context: host_power_usage
1974 + units: watts
1975 + algorithm: absolute
1976 + type: line
1977 + priority: 70210
1978 + lifecycle:
1979 + expire_after_cycles: 10
1980 + dimensions:
1981 + - selector: host_power_usage_power
1982 + name: power
1983 + - selector: host_power_usage_cap
1984 + name: cap
1985 + - id: host_power_capacity_usage
1986 + title: ESXi Host power capacity usage
1987 + context: host_power_capacity_usage
1988 + units: watts
1989 + algorithm: absolute
1990 + type: line
1991 + priority: 70211
1992 + lifecycle:
1993 + expire_after_cycles: 10
1994 + dimensions:
1995 + - selector: host_power_capacity_usage_used
1996 + name: used
1997 + - selector: host_power_capacity_usage_usable
1998 + name: usable
1999 + - selector: host_power_capacity_usage_idle
2000 + name: idle
2001 + - selector: host_power_capacity_usage_system
2002 + name: system
2003 + - selector: host_power_capacity_usage_vm
2004 + name: vm
2005 + - id: host_power_capacity_utilization
2006 + title: ESXi Host power capacity utilization
2007 + context: host_power_capacity_utilization
2008 + units: percentage
2009 + algorithm: absolute
2010 + type: line
2011 + priority: 70212
2012 + lifecycle:
2013 + expire_after_cycles: 10
2014 + dimensions:
2015 + - selector: host_power_capacity_utilization_used
2016 + name: used
2017 + options:
2018 + divisor: 100
2019 + - id: host_energy_usage
2020 + title: ESXi Host energy usage
2021 + context: host_energy_usage
2022 + units: joules
2023 + algorithm: absolute
2024 + type: line
2025 + priority: 70213
2026 + lifecycle:
2027 + expire_after_cycles: 10
2028 + dimensions:
2029 + - selector: host_energy_usage_energy
2030 + name: energy
2031 + - family: vms power
2032 + metrics:
2033 + - vm_power_usage_power
2034 + - vm_energy_usage_energy
2035 + chart_defaults:
2036 + instances:
2037 + by_labels:
2038 + - id
2039 + charts:
2040 + - id: vm_power_usage
2041 + title: Virtual Machine power usage
2042 + context: vm_power_usage
2043 + units: watts
2044 + algorithm: absolute
2045 + type: line
2046 + priority: 70220
2047 + lifecycle:
2048 + expire_after_cycles: 10
2049 + dimensions:
2050 + - selector: vm_power_usage_power
2051 + name: power
2052 + - id: vm_energy_usage
2053 + title: Virtual Machine energy usage
2054 + context: vm_energy_usage
2055 + units: joules
2056 + algorithm: absolute
2057 + type: line
2058 + priority: 70221
2059 + lifecycle:
2060 + expire_after_cycles: 10
2061 + dimensions:
2062 + - selector: vm_energy_usage_energy
2063 + name: energy
2064 + - family: vSAN clusters space
2065 + metrics:
2066 + - vsan_cluster_space_usage_total
2067 + - vsan_cluster_space_usage_free
2068 + - vsan_cluster_space_usage_used
2069 + - vsan_cluster_space_utilization_used
2070 + - vsan_cluster_health_status_green
2071 + - vsan_cluster_health_status_yellow
2072 + - vsan_cluster_health_status_red
2073 + - vsan_cluster_health_status_unknown
2074 + chart_defaults:
2075 + instances:
2076 + by_labels:
2077 + - id
2078 + charts:
2079 + - id: vsan_cluster_space_utilization
2080 + title: vSAN Cluster space utilization
2081 + context: vsan_cluster_space_utilization
2082 + units: percentage
2083 + algorithm: absolute
2084 + type: line
2085 + priority: 70105
2086 + lifecycle:
2087 + expire_after_cycles: 10
2088 + dimensions:
2089 + - selector: vsan_cluster_space_utilization_used
2090 + name: used
2091 + options:
2092 + divisor: 100
2093 + - id: vsan_cluster_space_usage
2094 + title: vSAN Cluster space usage
2095 + context: vsan_cluster_space_usage
2096 + units: bytes
2097 + algorithm: absolute
2098 + type: stacked
2099 + priority: 70106
2100 + lifecycle:
2101 + expire_after_cycles: 10
2102 + dimensions:
2103 + - selector: vsan_cluster_space_usage_used
2104 + name: used
2105 + - selector: vsan_cluster_space_usage_free
2106 + name: free
2107 + - selector: vsan_cluster_space_usage_total
2108 + name: total
2109 + options:
2110 + hidden: true
2111 + - id: vsan_cluster_health_status
2112 + title: vSAN Cluster health status
2113 + context: vsan_cluster_health_status
2114 + units: status
2115 + algorithm: absolute
2116 + type: line
2117 + priority: 70107
2118 + lifecycle:
2119 + expire_after_cycles: 10
2120 + dimensions:
2121 + - selector: vsan_cluster_health_status_green
2122 + name: green
2123 + - selector: vsan_cluster_health_status_yellow
2124 + name: yellow
2125 + - selector: vsan_cluster_health_status_red
2126 + name: red
2127 + - selector: vsan_cluster_health_status_unknown
2128 + name: unknown
2129 + - family: vSAN clusters performance
2130 + metrics:
2131 + - vsan_cluster_operations_read
2132 + - vsan_cluster_operations_write
2133 + - vsan_cluster_throughput_read
2134 + - vsan_cluster_throughput_write
2135 + - vsan_cluster_latency_read
2136 + - vsan_cluster_latency_write
2137 + - vsan_cluster_congestions
2138 + chart_defaults:
2139 + instances:
2140 + by_labels:
2141 + - id
2142 + charts:
2143 + - id: vsan_cluster_operations
2144 + title: vSAN Cluster operations
2145 + context: vsan_cluster_operations
2146 + units: operations/s
2147 + algorithm: absolute
2148 + type: line
2149 + priority: 70108
2150 + lifecycle:
2151 + expire_after_cycles: 10
2152 + dimensions:
2153 + - selector: vsan_cluster_operations_read
2154 + name: read
2155 + - selector: vsan_cluster_operations_write
2156 + name: write
2157 + - id: vsan_cluster_throughput
2158 + title: vSAN Cluster throughput
2159 + context: vsan_cluster_throughput
2160 + units: bytes/s
2161 + algorithm: absolute
2162 + type: area
2163 + priority: 70109
2164 + lifecycle:
2165 + expire_after_cycles: 10
2166 + dimensions:
2167 + - selector: vsan_cluster_throughput_read
2168 + name: read
2169 + - selector: vsan_cluster_throughput_write
2170 + name: write
2171 + - id: vsan_cluster_latency
2172 + title: vSAN Cluster latency
2173 + context: vsan_cluster_latency
2174 + units: microseconds
2175 + algorithm: absolute
2176 + type: line
2177 + priority: 70110
2178 + lifecycle:
2179 + expire_after_cycles: 10
2180 + dimensions:
2181 + - selector: vsan_cluster_latency_read
2182 + name: read
2183 + - selector: vsan_cluster_latency_write
2184 + name: write
2185 + - id: vsan_cluster_congestions
2186 + title: vSAN Cluster congestions
2187 + context: vsan_cluster_congestions
2188 + units: congestions/s
2189 + algorithm: absolute
2190 + type: line
2191 + priority: 70111
2192 + lifecycle:
2193 + expire_after_cycles: 10
2194 + dimensions:
2195 + - selector: vsan_cluster_congestions
2196 + name: congestions
2197 + - family: vSAN hosts performance
2198 + metrics:
2199 + - vsan_host_operations_read
2200 + - vsan_host_operations_write
2201 + - vsan_host_throughput_read
2202 + - vsan_host_throughput_write
2203 + - vsan_host_latency_read
2204 + - vsan_host_latency_write
2205 + - vsan_host_congestions
2206 + - vsan_host_cache_hit_rate
2207 + chart_defaults:
2208 + instances:
2209 + by_labels:
2210 + - id
2211 + charts:
2212 + - id: vsan_host_operations
2213 + title: vSAN Host operations
2214 + context: vsan_host_operations
2215 + units: operations/s
2216 + algorithm: absolute
2217 + type: line
2218 + priority: 70112
2219 + lifecycle:
2220 + expire_after_cycles: 10
2221 + dimensions:
2222 + - selector: vsan_host_operations_read
2223 + name: read
2224 + - selector: vsan_host_operations_write
2225 + name: write
2226 + - id: vsan_host_throughput
2227 + title: vSAN Host throughput
2228 + context: vsan_host_throughput
2229 + units: bytes/s
2230 + algorithm: absolute
2231 + type: area
2232 + priority: 70113
2233 + lifecycle:
2234 + expire_after_cycles: 10
2235 + dimensions:
2236 + - selector: vsan_host_throughput_read
2237 + name: read
2238 + - selector: vsan_host_throughput_write
2239 + name: write
2240 + - id: vsan_host_latency
2241 + title: vSAN Host latency
2242 + context: vsan_host_latency
2243 + units: microseconds
2244 + algorithm: absolute
2245 + type: line
2246 + priority: 70114
2247 + lifecycle:
2248 + expire_after_cycles: 10
2249 + dimensions:
2250 + - selector: vsan_host_latency_read
2251 + name: read
2252 + - selector: vsan_host_latency_write
2253 + name: write
2254 + - id: vsan_host_congestions
2255 + title: vSAN Host congestions
2256 + context: vsan_host_congestions
2257 + units: congestions/s
2258 + algorithm: absolute
2259 + type: line
2260 + priority: 70115
2261 + lifecycle:
2262 + expire_after_cycles: 10
2263 + dimensions:
2264 + - selector: vsan_host_congestions
2265 + name: congestions
2266 + - id: vsan_host_cache_hit_rate
2267 + title: vSAN Host client cache hit rate
2268 + context: vsan_host_cache_hit_rate
2269 + units: percentage
2270 + algorithm: absolute
2271 + type: line
2272 + priority: 70116
2273 + lifecycle:
2274 + expire_after_cycles: 10
2275 + dimensions:
2276 + - selector: vsan_host_cache_hit_rate
2277 + name: hit_rate
2278 + - family: vSAN VMs performance
2279 + metrics:
2280 + - vsan_vm_operations_read
2281 + - vsan_vm_operations_write
2282 + - vsan_vm_throughput_read
2283 + - vsan_vm_throughput_write
2284 + - vsan_vm_latency_read
2285 + - vsan_vm_latency_write
2286 + chart_defaults:
2287 + instances:
2288 + by_labels:
2289 + - id
2290 + charts:
2291 + - id: vsan_vm_operations
2292 + title: vSAN Virtual Machine operations
2293 + context: vsan_vm_operations
2294 + units: operations/s
2295 + algorithm: absolute
2296 + type: line
2297 + priority: 70117
2298 + lifecycle:
2299 + expire_after_cycles: 10
2300 + dimensions:
2301 + - selector: vsan_vm_operations_read
2302 + name: read
2303 + - selector: vsan_vm_operations_write
2304 + name: write
2305 + - id: vsan_vm_throughput
2306 + title: vSAN Virtual Machine throughput
2307 + context: vsan_vm_throughput
2308 + units: bytes/s
2309 + algorithm: absolute
2310 + type: area
2311 + priority: 70118
2312 + lifecycle:
2313 + expire_after_cycles: 10
2314 + dimensions:
2315 + - selector: vsan_vm_throughput_read
2316 + name: read
2317 + - selector: vsan_vm_throughput_write
2318 + name: write
2319 + - id: vsan_vm_latency
2320 + title: vSAN Virtual Machine latency
2321 + context: vsan_vm_latency
2322 + units: microseconds
2323 + algorithm: absolute
2324 + type: line
2325 + priority: 70119
2326 + lifecycle:
2327 + expire_after_cycles: 10
2328 + dimensions:
2329 + - selector: vsan_vm_latency_read
2330 + name: read
2331 + - selector: vsan_vm_latency_write
2332 + name: write
src/go/plugin/go.d/collector/vsphere/client/client.go
+291 -24
@@ -4,19 +4,28 @@ package client
4
5 import (
6 "context"
7 + "errors"
8 + "fmt"
9 "net/http"
10 "net/url"
11 + "sync"
12 "time"
13
14 "github.com/vmware/govmomi"
15 + "github.com/vmware/govmomi/object"
16 "github.com/vmware/govmomi/performance"
17 "github.com/vmware/govmomi/property"
18 "github.com/vmware/govmomi/session"
19 + "github.com/vmware/govmomi/vapi/rest"
20 + "github.com/vmware/govmomi/vapi/tags"
21 "github.com/vmware/govmomi/view"
22 "github.com/vmware/govmomi/vim25"
23 "github.com/vmware/govmomi/vim25/mo"
24 "github.com/vmware/govmomi/vim25/soap"
25 "github.com/vmware/govmomi/vim25/types"
26 + vsanapi "github.com/vmware/govmomi/vsan"
27 + vsanmethods "github.com/vmware/govmomi/vsan/methods"
28 + vsantypes "github.com/vmware/govmomi/vsan/types"
29
30 "github.com/netdata/netdata/go/plugins/pkg/tlscfg"
31 )
@@ -28,6 +37,8 @@ const (
37 hostSystem = "HostSystem"
38 virtualMachine = "VirtualMachine"
39 datastoreType = "Datastore"
40 + networkType = "Network"
41 + storagePodType = "StoragePod"
42 resourcePoolType = "ResourcePool"
43
44 maxIdleConnections = 32
@@ -42,29 +53,37 @@ type Config struct {
53 }
54
55 type Client struct {
45 - client *govmomi.Client
46 - root *view.ContainerView
47 - perf *performance.Manager
56 + client *govmomi.Client
57 + root *view.ContainerView
58 + perf *performance.Manager
59 + userInfo *url.Userinfo
60 + rest *rest.Client
61 + tags *tags.Manager
62 + vsan *vsanapi.Client
63 + lazyMu sync.Mutex
64 }
65
66 func newSoapClient(config Config) (*soap.Client, error) {
67 soapURL, err := soap.ParseURL(config.URL)
52 - if err != nil || soapURL == nil {
53 - return nil, err
68 + if err != nil {
69 + return nil, fmt.Errorf("parse config option url for vSphere SOAP endpoint: %w", err)
70 + }
71 + if soapURL == nil {
72 + return nil, errors.New("parse config option url for vSphere SOAP endpoint: empty SOAP URL")
73 }
74 soapURL.User = url.UserPassword(config.User, config.Password)
75 soapClient := soap.NewClient(soapURL, config.TLSConfig.InsecureSkipVerify)
76
77 tlsConfig, err := tlscfg.NewTLSConfig(config.TLSConfig)
78 if err != nil {
60 - return nil, err
79 + return nil, fmt.Errorf("build TLS configuration from tls_* options: %w", err)
80 }
81 if tlsConfig != nil && len(tlsConfig.Certificates) > 0 {
82 soapClient.SetCertificate(tlsConfig.Certificates[0])
83 }
84 if config.TLSConfig.TLSCA != "" {
85 if err := soapClient.SetRootCAs(config.TLSConfig.TLSCA); err != nil {
67 - return nil, err
86 + return nil, fmt.Errorf("load tls_ca certificate bundle %q for vSphere SOAP client: %w", config.TLSConfig.TLSCA, err)
87 }
88 }
89
@@ -82,6 +101,8 @@ func newContainerView(ctx context.Context, client *govmomi.Client) (*view.Contai
101 return viewManager.CreateContainerView(ctx, client.ServiceContent.RootFolder, []string{}, true)
102 }
103
104 +var createContainerView = newContainerView
105 +
106 func newPerformanceManager(client *vim25.Client) *performance.Manager {
107 perfManager := performance.NewManager(client)
108 perfManager.Sort = true
@@ -92,12 +113,12 @@ func New(config Config) (*Client, error) {
113 ctx := context.Background()
114 soapClient, err := newSoapClient(config)
115 if err != nil {
95 - return nil, err
116 + return nil, fmt.Errorf("initialize vSphere SOAP client: %w", err)
117 }
118
119 vimClient, err := vim25.NewClient(ctx, soapClient)
120 if err != nil {
100 - return nil, err
121 + return nil, fmt.Errorf("initialize vSphere vim25 client and retrieve service content: %w", err)
122 }
123
124 vmomiClient := &govmomi.Client{
@@ -110,47 +131,98 @@ func New(config Config) (*Client, error) {
131
132 err = vmomiClient.Login(ctx, userInfo)
133 if err != nil {
113 - return nil, err
134 + return nil, fmt.Errorf("login to vSphere API with configured username: %w", err)
135 }
136
116 - containerView, err := newContainerView(ctx, vmomiClient)
137 + containerView, err := createContainerView(ctx, vmomiClient)
138 if err != nil {
118 - return nil, err
139 + return nil, fmt.Errorf("create root vSphere container view: %w", errors.Join(err, vmomiClient.Logout(ctx)))
140 }
141
142 perfManager := newPerformanceManager(vimClient)
143
144 client := &Client{
124 - client: vmomiClient,
125 - perf: perfManager,
126 - root: containerView,
145 + client: vmomiClient,
146 + perf: perfManager,
147 + root: containerView,
148 + userInfo: userInfo,
149 }
150
151 return client, nil
152 }
153
154 func (c *Client) IsSessionActive() (bool, error) {
133 - return c.client.SessionManager.SessionIsActive(context.Background())
155 + active, err := c.client.SessionManager.SessionIsActive(context.Background())
156 + if err != nil {
157 + return false, fmt.Errorf("check vSphere SOAP session activity: %w", err)
158 + }
159 + return active, nil
160 }
161
162 func (c *Client) Version() string {
163 return c.client.ServiceContent.About.Version
164 }
165
166 +func (c *Client) InstanceUUID() string {
167 + return c.client.ServiceContent.About.InstanceUuid
168 +}
169 +
170 func (c *Client) Login(userinfo *url.Userinfo) error {
141 - return c.client.Login(context.Background(), userinfo)
171 + if err := c.client.Login(context.Background(), userinfo); err != nil {
172 + return fmt.Errorf("login to vSphere SOAP API: %w", err)
173 + }
174 + return nil
175 }
176
177 func (c *Client) Logout() error {
145 - return c.client.Logout(context.Background())
178 + if err := c.client.Logout(context.Background()); err != nil {
179 + return fmt.Errorf("logout from vSphere SOAP API: %w", err)
180 + }
181 + return nil
182 +}
183 +
184 +func (c *Client) Close() error {
185 + if c == nil {
186 + return nil
187 + }
188 +
189 + ctx := context.Background()
190 + var err error
191 + if c.root != nil {
192 + if e := c.root.Destroy(ctx); e != nil {
193 + err = errors.Join(err, fmt.Errorf("destroy root vSphere container view: %w", e))
194 + }
195 + c.root = nil
196 + }
197 + c.lazyMu.Lock()
198 + if c.rest != nil {
199 + if e := c.rest.Logout(ctx); e != nil {
200 + err = errors.Join(err, fmt.Errorf("logout from vSphere REST API: %w", e))
201 + }
202 + c.rest = nil
203 + c.tags = nil
204 + }
205 + c.vsan = nil
206 + c.userInfo = nil
207 + c.lazyMu.Unlock()
208 + if c.client != nil {
209 + if e := c.client.Logout(ctx); e != nil {
210 + err = errors.Join(err, fmt.Errorf("logout from vSphere SOAP API: %w", e))
211 + }
212 + }
213 + return err
214 }
215
216 func (c *Client) PerformanceMetrics(pqs []types.PerfQuerySpec) ([]performance.EntityMetric, error) {
217 metrics, err := c.perf.Query(context.Background(), pqs)
218 if err != nil {
151 - return nil, err
219 + return nil, fmt.Errorf("query vSphere performance manager for %d perf query specs: %w", len(pqs), err)
220 }
153 - return c.perf.ToMetricSeries(context.Background(), metrics)
221 + series, err := c.perf.ToMetricSeries(context.Background(), metrics)
222 + if err != nil {
223 + return nil, fmt.Errorf("convert vSphere performance samples for %d perf query specs: %w", len(pqs), err)
224 + }
225 + return series, nil
226 }
227
228 func (c *Client) Datacenters(pathSet ...string) (dcs []mo.Datacenter, err error) {
@@ -183,6 +255,16 @@ func (c *Client) Datastores(pathSet ...string) (datastores []mo.Datastore, err e
255 return
256 }
257
258 +func (c *Client) Networks(pathSet ...string) (networks []mo.Network, err error) {
259 + err = c.root.Retrieve(context.Background(), []string{networkType}, pathSet, &networks)
260 + return
261 +}
262 +
263 +func (c *Client) StoragePods(pathSet ...string) (pods []mo.StoragePod, err error) {
264 + err = c.root.Retrieve(context.Background(), []string{storagePodType}, pathSet, &pods)
265 + return
266 +}
267 +
268 func (c *Client) DatastoresByRef(refs []types.ManagedObjectReference, pathSet ...string) ([]mo.Datastore, error) {
269 if len(refs) == 0 {
270 return nil, nil
@@ -190,7 +272,10 @@ func (c *Client) DatastoresByRef(refs []types.ManagedObjectReference, pathSet ..
272 var datastores []mo.Datastore
273 pc := property.DefaultCollector(c.client.Client)
274 err := pc.Retrieve(context.Background(), refs, pathSet, &datastores)
193 - return datastores, err
275 + if err != nil {
276 + return nil, fmt.Errorf("retrieve datastore properties for %d refs pathSet=%v: %w", len(refs), pathSet, err)
277 + }
278 + return datastores, nil
279 }
280
281 func (c *Client) ClustersByRef(refs []types.ManagedObjectReference, pathSet ...string) ([]mo.ClusterComputeResource, error) {
@@ -200,7 +285,10 @@ func (c *Client) ClustersByRef(refs []types.ManagedObjectReference, pathSet ...s
285 var clusters []mo.ClusterComputeResource
286 pc := property.DefaultCollector(c.client.Client)
287 err := pc.Retrieve(context.Background(), refs, pathSet, &clusters)
203 - return clusters, err
288 + if err != nil {
289 + return nil, fmt.Errorf("retrieve cluster properties for %d refs pathSet=%v: %w", len(refs), pathSet, err)
290 + }
291 + return clusters, nil
292 }
293
294 func (c *Client) ResourcePools(pathSet ...string) (pools []mo.ResourcePool, err error) {
@@ -215,9 +303,188 @@ func (c *Client) ResourcePoolsByRef(refs []types.ManagedObjectReference, pathSet
303 var pools []mo.ResourcePool
304 pc := property.DefaultCollector(c.client.Client)
305 err := pc.Retrieve(context.Background(), refs, pathSet, &pools)
218 - return pools, err
306 + if err != nil {
307 + return nil, fmt.Errorf("retrieve resource pool properties for %d refs pathSet=%v: %w", len(refs), pathSet, err)
308 + }
309 + return pools, nil
310 +}
311 +
312 +func (c *Client) CustomFields() ([]types.CustomFieldDef, error) {
313 + m, err := object.GetCustomFieldsManager(c.client.Client)
314 + if err != nil {
315 + return nil, fmt.Errorf("get vSphere custom fields manager: %w", err)
316 + }
317 + fields, err := m.Field(context.Background())
318 + if err != nil {
319 + return nil, fmt.Errorf("list vSphere custom field definitions: %w", err)
320 + }
321 + return fields, nil
322 +}
323 +
324 +func (c *Client) TagsByRef(refs []types.ManagedObjectReference) (map[types.ManagedObjectReference]map[string][]string, error) {
325 + if len(refs) == 0 {
326 + return nil, nil
327 + }
328 +
329 + ctx := context.Background()
330 + manager, err := c.tagManager(ctx)
331 + if err != nil {
332 + return nil, fmt.Errorf("initialize vSphere tag manager: %w", err)
333 + }
334 +
335 + categories, err := manager.GetCategories(ctx)
336 + if err != nil {
337 + return nil, fmt.Errorf("list vSphere tag categories: %w", err)
338 + }
339 + categoriesByID := make(map[string]string, len(categories))
340 + for _, category := range categories {
341 + categoriesByID[category.ID] = category.Name
342 + }
343 +
344 + tagList, err := manager.GetTags(ctx)
345 + if err != nil {
346 + return nil, fmt.Errorf("list vSphere tags: %w", err)
347 + }
348 + tagsByID := make(map[string]tags.Tag, len(tagList))
349 + for _, tag := range tagList {
350 + tagsByID[tag.ID] = tag
351 + }
352 +
353 + out := make(map[types.ManagedObjectReference]map[string][]string)
354 + for i := 0; i < len(refs); i += maxTagAssociationBatchSize {
355 + end := min(i+maxTagAssociationBatchSize, len(refs))
356 + batch := make([]mo.Reference, 0, end-i)
357 + for _, ref := range refs[i:end] {
358 + batch = append(batch, ref)
359 + }
360 +
361 + attached, err := manager.ListAttachedTagsOnObjects(ctx, batch)
362 + if err != nil {
363 + return nil, fmt.Errorf("list vSphere tag attachments for refs batch offset=%d size=%d: %w", i, len(batch), err)
364 + }
365 + for _, objectTags := range attached {
366 + ref := objectTags.ObjectID.Reference()
367 + for _, tagID := range objectTags.TagIDs {
368 + tag, ok := tagsByID[tagID]
369 + if !ok || tag.Name == "" {
370 + continue
371 + }
372 + category := categoriesByID[tag.CategoryID]
373 + if category == "" {
374 + continue
375 + }
376 + if out[ref] == nil {
377 + out[ref] = make(map[string][]string)
378 + }
379 + out[ref][category] = append(out[ref][category], tag.Name)
380 + }
381 + }
382 + }
383 +
384 + return out, nil
385 +}
386 +
387 +func (c *Client) tagManager(ctx context.Context) (*tags.Manager, error) {
388 + c.lazyMu.Lock()
389 + defer c.lazyMu.Unlock()
390 +
391 + if c.tags != nil {
392 + return c.tags, nil
393 + }
394 + restClient := rest.NewClient(c.client.Client)
395 + if err := restClient.Login(ctx, c.userInfo); err != nil {
396 + return nil, fmt.Errorf("login to vSphere REST API for tag collection: %w", err)
397 + }
398 + c.rest = restClient
399 + c.tags = tags.NewManager(restClient)
400 + return c.tags, nil
401 +}
402 +
403 +func (c *Client) VSANPerfMetrics(cluster types.ManagedObjectReference, specs []vsantypes.VsanPerfQuerySpec) ([]vsantypes.VsanPerfEntityMetricCSV, error) {
404 + if len(specs) == 0 {
405 + return nil, nil
406 + }
407 + ctx := context.Background()
408 + cli, err := c.vsanClient(ctx)
409 + if err != nil {
410 + return nil, fmt.Errorf("initialize vSAN client for cluster %s: %w", cluster.Value, err)
411 + }
412 + metrics, err := cli.VsanPerfQueryPerf(ctx, &cluster, specs)
413 + if err != nil {
414 + return nil, fmt.Errorf("query vSAN performance metrics for cluster %s with %d specs: %w", cluster.Value, len(specs), err)
415 + }
416 + return metrics, nil
417 +}
418 +
419 +func (c *Client) VSANSpaceUsage(cluster types.ManagedObjectReference) (*vsantypes.VsanSpaceUsage, error) {
420 + ctx := context.Background()
421 + cli, err := c.vsanClient(ctx)
422 + if err != nil {
423 + return nil, fmt.Errorf("initialize vSAN client for cluster %s: %w", cluster.Value, err)
424 + }
425 + req := vsantypes.VsanQuerySpaceUsage{
426 + This: vsanSpaceReportSystemInstance,
427 + Cluster: cluster,
428 + }
429 + res, err := vsanmethods.VsanQuerySpaceUsage(ctx, cli, &req)
430 + if err != nil {
431 + return nil, fmt.Errorf("query vSAN space usage for cluster %s: %w", cluster.Value, err)
432 + }
433 + return &res.Returnval, nil
434 +}
435 +
436 +func (c *Client) VSANHealth(cluster types.ManagedObjectReference) (string, error) {
437 + ctx := context.Background()
438 + cli, err := c.vsanClient(ctx)
439 + if err != nil {
440 + return "", fmt.Errorf("initialize vSAN client for cluster %s: %w", cluster.Value, err)
441 + }
442 + fetchFromCache := true
443 + req := vsantypes.VsanQueryVcClusterHealthSummary{
444 + This: vsanClusterHealthSystemInstance,
445 + Cluster: &cluster,
446 + Fields: []string{"overallHealth", "overallHealthDescription"},
447 + FetchFromCache: &fetchFromCache,
448 + }
449 + res, err := vsanmethods.VsanQueryVcClusterHealthSummary(ctx, cli, &req)
450 + if err != nil {
451 + return "", fmt.Errorf("query vSAN health summary for cluster %s: %w", cluster.Value, err)
452 + }
453 + return res.Returnval.OverallHealth, nil
454 +}
455 +
456 +func (c *Client) vsanClient(ctx context.Context) (*vsanapi.Client, error) {
457 + c.lazyMu.Lock()
458 + defer c.lazyMu.Unlock()
459 +
460 + if c.vsan != nil {
461 + return c.vsan, nil
462 + }
463 + cli, err := vsanapi.NewClient(ctx, c.client.Client)
464 + if err != nil {
465 + return nil, fmt.Errorf("create govmomi vSAN API client: %w", err)
466 + }
467 + c.vsan = cli
468 + return c.vsan, nil
469 }
470
471 func (c *Client) CounterInfoByName() (map[string]*types.PerfCounterInfo, error) {
222 - return c.perf.CounterInfoByName(context.Background())
472 + counters, err := c.perf.CounterInfoByName(context.Background())
473 + if err != nil {
474 + return nil, fmt.Errorf("list vSphere performance counter registry: %w", err)
475 + }
476 + return counters, nil
477 }
478 +
479 +const maxTagAssociationBatchSize = 2000
480 +
481 +var (
482 + vsanSpaceReportSystemInstance = types.ManagedObjectReference{
483 + Type: "VsanSpaceReportSystem",
484 + Value: "vsan-cluster-space-report-system",
485 + }
486 + vsanClusterHealthSystemInstance = types.ManagedObjectReference{
487 + Type: "VsanVcClusterHealthSystem",
488 + Value: "vsan-cluster-health-system",
489 + }
490 +)
src/go/plugin/go.d/collector/vsphere/client/client_test.go
+96 -33
@@ -3,14 +3,20 @@
3 package client
4
5 import (
6 + "context"
7 "crypto/tls"
8 + "errors"
9 "net/url"
10 "testing"
11 "time"
12
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
15 + "github.com/vmware/govmomi"
16 + "github.com/vmware/govmomi/property"
17 "github.com/vmware/govmomi/simulator"
18 + _ "github.com/vmware/govmomi/vapi/simulator"
19 + "github.com/vmware/govmomi/view"
20 "github.com/vmware/govmomi/vim25/mo"
21 "github.com/vmware/govmomi/vim25/types"
22
@@ -77,49 +83,91 @@ func TestClient_Logout(t *testing.T) {
83 assert.False(t, v)
84 }
85
80 -func TestClient_Datacenters(t *testing.T) {
81 - client, teardown := prepareClient(t)
82 - defer teardown()
86 +func TestClient_Close(t *testing.T) {
87 + model, srv := createSim(t)
88 + defer model.Remove()
89 + defer srv.Close()
90
84 - dcs, err := client.Datacenters()
85 - assert.NoError(t, err)
86 - assert.NotEmpty(t, dcs)
87 -}
91 + client := newClient(t, srv.URL)
92 + _, err := client.tagManager(context.Background())
93 + require.NoError(t, err)
94 + require.NotNil(t, client.rest)
95 + require.NotNil(t, client.tags)
96
89 -func TestClient_Folders(t *testing.T) {
90 - client, teardown := prepareClient(t)
91 - defer teardown()
97 + assert.NoError(t, client.Close())
98 + assert.Nil(t, client.root)
99 + assert.Nil(t, client.rest)
100 + assert.Nil(t, client.tags)
101 + assert.Nil(t, client.vsan)
102 + assert.Nil(t, client.userInfo)
103
93 - folders, err := client.Folders()
104 + v, err := client.IsSessionActive()
105 assert.NoError(t, err)
95 - assert.NotEmpty(t, folders)
96 -}
97 -
98 -func TestClient_ComputeResources(t *testing.T) {
99 - client, teardown := prepareClient(t)
100 - defer teardown()
106 + assert.False(t, v)
107
102 - computes, err := client.ComputeResources()
103 - assert.NoError(t, err)
104 - assert.NotEmpty(t, computes)
108 + control := newClient(t, srv.URL)
109 + defer func() { _ = control.Close() }()
110 + require.Len(t, sessionList(t, control), 1)
111 }
112
107 -func TestClient_Hosts(t *testing.T) {
108 - client, teardown := prepareClient(t)
109 - defer teardown()
113 +func TestNew_LogsOutOnContainerViewFailure(t *testing.T) {
114 + model, srv := createSim(t)
115 + defer model.Remove()
116 + defer srv.Close()
117
111 - hosts, err := client.Hosts()
112 - assert.NoError(t, err)
113 - assert.NotEmpty(t, hosts)
114 -}
118 + origCreateContainerView := createContainerView
119 + createContainerView = func(context.Context, *govmomi.Client) (*view.ContainerView, error) {
120 + return nil, errors.New("create container view failed")
121 + }
122 + defer func() { createContainerView = origCreateContainerView }()
123
116 -func TestClient_VirtualMachines(t *testing.T) {
117 - client, teardown := prepareClient(t)
118 - defer teardown()
124 + client, err := New(Config{
125 + URL: srv.URL.String(),
126 + User: "admin",
127 + Password: "password",
128 + Timeout: time.Second * 3,
129 + TLSConfig: tlscfg.TLSConfig{InsecureSkipVerify: true},
130 + })
131 + require.Nil(t, client)
132 + require.ErrorContains(t, err, "create container view failed")
133 +
134 + createContainerView = origCreateContainerView
135 + control := newClient(t, srv.URL)
136 + defer func() { _ = control.Close() }()
137 + require.Len(t, sessionList(t, control), 1)
138 +}
139 +
140 +func TestClient_InventoryMethods(t *testing.T) {
141 + tests := map[string]struct {
142 + collect func(*Client) (any, error)
143 + }{
144 + "datacenters": {
145 + collect: func(c *Client) (any, error) { return c.Datacenters() },
146 + },
147 + "folders": {
148 + collect: func(c *Client) (any, error) { return c.Folders() },
149 + },
150 + "compute resources": {
151 + collect: func(c *Client) (any, error) { return c.ComputeResources() },
152 + },
153 + "hosts": {
154 + collect: func(c *Client) (any, error) { return c.Hosts() },
155 + },
156 + "virtual machines": {
157 + collect: func(c *Client) (any, error) { return c.VirtualMachines() },
158 + },
159 + }
160
120 - vms, err := client.VirtualMachines()
121 - assert.NoError(t, err)
122 - assert.NotEmpty(t, vms)
161 + for name, tc := range tests {
162 + t.Run(name, func(t *testing.T) {
163 + client, teardown := prepareClient(t)
164 + defer teardown()
165 +
166 + got, err := tc.collect(client)
167 + assert.NoError(t, err)
168 + assert.NotEmpty(t, got)
169 + })
170 + }
171 }
172
173 func TestClient_PerformanceMetrics(t *testing.T) {
@@ -151,11 +199,26 @@ func newClient(t *testing.T, vCenterURL *url.URL) *Client {
199 return client
200 }
201
202 +func sessionList(t *testing.T, client *Client) []types.UserSession {
203 + t.Helper()
204 +
205 + var sm mo.SessionManager
206 + err := property.DefaultCollector(client.client.Client).RetrieveOne(
207 + context.Background(),
208 + *client.client.ServiceContent.SessionManager,
209 + []string{"sessionList"},
210 + &sm,
211 + )
212 + require.NoError(t, err)
213 + return sm.SessionList
214 +}
215 +
216 func createSim(t *testing.T) (*simulator.Model, *simulator.Server) {
217 model := simulator.VPX()
218 err := model.Create()
219 require.NoError(t, err)
220 model.Service.TLS = new(tls.Config)
221 + model.Service.RegisterEndpoints = true
222 return model, model.Service.NewServer()
223 }
224
src/go/plugin/go.d/collector/vsphere/collect.go
+593 -156
@@ -3,10 +3,12 @@
3 package vsphere
4
5 import (
6 - "errors"
6 "fmt"
7 + "strings"
8 "time"
9 + "unicode"
10
11 + "github.com/netdata/netdata/go/plugins/pkg/metrix"
12 rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
13 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/oldmetrix"
14
@@ -18,120 +20,490 @@ import (
20 // ManagedEntityStatus
21 var overallStatuses = []string{"green", "red", "yellow", "gray"}
22
21 -func (c *Collector) collect() (map[string]int64, error) {
22 - c.collectionLock.Lock()
23 - defer c.collectionLock.Unlock()
23 +var vmPowerStates = []struct {
24 + value string
25 + key string
26 +}{
27 + {value: string(types.VirtualMachinePowerStatePoweredOn), key: "poweredOn"},
28 + {value: string(types.VirtualMachinePowerStatePoweredOff), key: "poweredOff"},
29 + {value: string(types.VirtualMachinePowerStateSuspended), key: "suspended"},
30 +}
31 +
32 +var vmConnectionStates = []struct {
33 + value string
34 + key string
35 +}{
36 + {value: string(types.VirtualMachineConnectionStateConnected), key: "connected"},
37 + {value: string(types.VirtualMachineConnectionStateDisconnected), key: "disconnected"},
38 + {value: string(types.VirtualMachineConnectionStateOrphaned), key: "orphaned"},
39 + {value: string(types.VirtualMachineConnectionStateInaccessible), key: "inaccessible"},
40 + {value: string(types.VirtualMachineConnectionStateInvalid), key: "invalid"},
41 +}
42 +
43 +var vmToolsRunningStatuses = []struct {
44 + value string
45 + key string
46 +}{
47 + {value: string(types.VirtualMachineToolsRunningStatusGuestToolsRunning), key: "running"},
48 + {value: string(types.VirtualMachineToolsRunningStatusGuestToolsNotRunning), key: "notRunning"},
49 + {value: string(types.VirtualMachineToolsRunningStatusGuestToolsExecutingScripts), key: "executingScripts"},
50 +}
51 +
52 +var vmToolsVersionStatuses = []struct {
53 + value string
54 + key string
55 +}{
56 + {value: string(types.VirtualMachineToolsVersionStatusGuestToolsCurrent), key: "current"},
57 + {value: string(types.VirtualMachineToolsVersionStatusGuestToolsNeedUpgrade), key: "needUpgrade"},
58 + {value: string(types.VirtualMachineToolsVersionStatusGuestToolsNotInstalled), key: "notInstalled"},
59 + {value: string(types.VirtualMachineToolsVersionStatusGuestToolsUnmanaged), key: "unmanaged"},
60 + {value: string(types.VirtualMachineToolsVersionStatusGuestToolsTooOld), key: "tooOld"},
61 + {value: string(types.VirtualMachineToolsVersionStatusGuestToolsSupportedOld), key: "supportedOld"},
62 + {value: string(types.VirtualMachineToolsVersionStatusGuestToolsSupportedNew), key: "supportedNew"},
63 + {value: string(types.VirtualMachineToolsVersionStatusGuestToolsTooNew), key: "tooNew"},
64 + {value: string(types.VirtualMachineToolsVersionStatusGuestToolsBlacklisted), key: "blacklisted"},
65 +}
66 +
67 +var hostPowerStates = []struct {
68 + value string
69 + key string
70 +}{
71 + {value: string(types.HostSystemPowerStatePoweredOn), key: "poweredOn"},
72 + {value: string(types.HostSystemPowerStatePoweredOff), key: "poweredOff"},
73 + {value: string(types.HostSystemPowerStateStandBy), key: "standBy"},
74 + {value: string(types.HostSystemPowerStateUnknown), key: "unknown"},
75 +}
76 +
77 +var hostConnectionStates = []struct {
78 + value string
79 + key string
80 +}{
81 + {value: string(types.HostSystemConnectionStateConnected), key: "connected"},
82 + {value: string(types.HostSystemConnectionStateNotResponding), key: "notResponding"},
83 + {value: string(types.HostSystemConnectionStateDisconnected), key: "disconnected"},
84 +}
85 +
86 +var datastoreMaintenanceModes = []struct {
87 + value string
88 + key string
89 +}{
90 + {value: string(types.DatastoreSummaryMaintenanceModeStateNormal), key: "normal"},
91 + {value: string(types.DatastoreSummaryMaintenanceModeStateEnteringMaintenance), key: "enteringMaintenance"},
92 + {value: string(types.DatastoreSummaryMaintenanceModeStateInMaintenance), key: "inMaintenance"},
93 +}
94 +
95 +var clusterDRSModes = []struct {
96 + value string
97 + key string
98 +}{
99 + {value: string(types.DrsBehaviorManual), key: "manual"},
100 + {value: string(types.DrsBehaviorPartiallyAutomated), key: "partiallyAutomated"},
101 + {value: string(types.DrsBehaviorFullyAutomated), key: "fullyAutomated"},
102 +}
103 +
104 +var clusterHAServiceStates = []struct {
105 + value string
106 + key string
107 +}{
108 + {value: string(types.ClusterDasConfigInfoServiceStateEnabled), key: "enabled"},
109 + {value: string(types.ClusterDasConfigInfoServiceStateDisabled), key: "disabled"},
110 +}
111 +
112 +var clusterHAVMMonitoringStates = []struct {
113 + value string
114 + key string
115 +}{
116 + {value: string(types.ClusterDasConfigInfoVmMonitoringStateVmMonitoringDisabled), key: "vmMonitoringDisabled"},
117 + {value: string(types.ClusterDasConfigInfoVmMonitoringStateVmMonitoringOnly), key: "vmMonitoringOnly"},
118 + {value: string(types.ClusterDasConfigInfoVmMonitoringStateVmAndAppMonitoring), key: "vmAndAppMonitoring"},
119 +}
120 +
121 +var hostPerfMetricByCounter = map[string]string{
122 + "cpu.usage.average": "host_cpu_utilization_used",
123 + "mem.usage.average": "host_mem_utilization_used",
124 + "mem.granted.average": "host_mem_usage_granted",
125 + "mem.consumed.average": "host_mem_usage_consumed",
126 + "mem.active.average": "host_mem_usage_active",
127 + "mem.shared.average": "host_mem_usage_shared",
128 + "mem.sharedcommon.average": "host_mem_usage_sharedcommon",
129 + "mem.swapinRate.average": "host_mem_swap_io_in",
130 + "mem.swapoutRate.average": "host_mem_swap_io_out",
131 + "disk.read.average": "host_disk_io_read",
132 + "disk.write.average": "host_disk_io_write",
133 + "disk.maxTotalLatency.latest": "host_disk_max_latency_latency",
134 + "net.bytesRx.average": "host_net_traffic_received",
135 + "net.bytesTx.average": "host_net_traffic_sent",
136 + "net.packetsRx.summation": "host_net_packets_received",
137 + "net.packetsTx.summation": "host_net_packets_sent",
138 + "net.droppedRx.summation": "host_net_drops_received",
139 + "net.droppedTx.summation": "host_net_drops_sent",
140 + "net.errorsRx.summation": "host_net_errors_received",
141 + "net.errorsTx.summation": "host_net_errors_sent",
142 + "sys.uptime.latest": "host_system_uptime_uptime",
143 +}
144 +
145 +var vmPerfMetricByCounter = map[string]string{
146 + "cpu.usage.average": "vm_cpu_utilization_used",
147 + "mem.usage.average": "vm_mem_utilization_used",
148 + "mem.granted.average": "vm_mem_usage_granted",
149 + "mem.consumed.average": "vm_mem_usage_consumed",
150 + "mem.active.average": "vm_mem_usage_active",
151 + "mem.shared.average": "vm_mem_usage_shared",
152 + "mem.swapped.average": "vm_mem_swap_usage_swapped",
153 + "mem.swapinRate.average": "vm_mem_swap_io_in",
154 + "mem.swapoutRate.average": "vm_mem_swap_io_out",
155 + "disk.read.average": "vm_disk_io_read",
156 + "disk.write.average": "vm_disk_io_write",
157 + "disk.maxTotalLatency.latest": "vm_disk_max_latency_latency",
158 + "net.bytesRx.average": "vm_net_traffic_received",
159 + "net.bytesTx.average": "vm_net_traffic_sent",
160 + "net.packetsRx.summation": "vm_net_packets_received",
161 + "net.packetsTx.summation": "vm_net_packets_sent",
162 + "net.droppedRx.summation": "vm_net_drops_received",
163 + "net.droppedTx.summation": "vm_net_drops_sent",
164 + "sys.uptime.latest": "vm_system_uptime_uptime",
165 +}
166 +
167 +var datastorePerfMetricByCounter = map[string]string{
168 + "datastore.read.average": "datastore_disk_io_read",
169 + "datastore.write.average": "datastore_disk_io_write",
170 + "datastore.numberReadAveraged.average": "datastore_disk_iops_reads",
171 + "datastore.numberWriteAveraged.average": "datastore_disk_iops_writes",
172 + "datastore.totalReadLatency.average": "datastore_disk_latency_read",
173 + "datastore.totalWriteLatency.average": "datastore_disk_latency_write",
174 +}
175 +
176 +var clusterPerfMetricByCounter = map[string]string{
177 + "cpu.usage.average": "cluster_cpu_utilization_used",
178 + "cpu.usagemhz.average": "cluster_cpu_usage_used",
179 + "cpu.totalmhz.average": "cluster_cpu_usage_total",
180 + "mem.usage.average": "cluster_mem_utilization_used",
181 + "mem.consumed.average": "cluster_mem_usage_consumed",
182 + "mem.active.average": "cluster_mem_usage_active",
183 + "mem.granted.average": "cluster_mem_usage_granted",
184 + "mem.shared.average": "cluster_mem_usage_shared",
185 + "mem.overhead.average": "cluster_mem_usage_overhead",
186 + "mem.swapused.average": "cluster_mem_usage_swap_used",
187 + "clusterServices.effectivecpu.average": "cluster_services_effective_cpu_effective_cpu",
188 + "clusterServices.effectivemem.average": "cluster_services_effective_mem_effective_mem",
189 + "clusterServices.cpufairness.latest": "cluster_services_fairness_cpu",
190 + "clusterServices.memfairness.latest": "cluster_services_fairness_memory",
191 + "clusterServices.failover.latest": "cluster_services_failover_failures_tolerable",
192 + "vmop.numVMotion.latest": "cluster_vm_migrations_vmotion",
193 + "vmop.numSVMotion.latest": "cluster_vm_migrations_svmotion",
194 + "vmop.numXVMotion.latest": "cluster_vm_migrations_xvmotion",
195 + "vmop.numPoweron.latest": "cluster_vm_lifecycle_poweron",
196 + "vmop.numPoweroff.latest": "cluster_vm_lifecycle_poweroff",
197 + "vmop.numCreate.latest": "cluster_vm_lifecycle_create",
198 + "vmop.numDestroy.latest": "cluster_vm_lifecycle_destroy",
199 + "vmop.numClone.latest": "cluster_vm_lifecycle_clone",
200 + "vmop.numDeploy.latest": "cluster_vm_lifecycle_deploy",
201 + "vmop.numReconfigure.latest": "cluster_vm_management_reconfigure",
202 + "vmop.numReset.latest": "cluster_vm_management_reset",
203 + "vmop.numSuspend.latest": "cluster_vm_management_suspend",
204 + "vmop.numRegister.latest": "cluster_vm_management_register",
205 + "vmop.numUnregister.latest": "cluster_vm_management_unregister",
206 + "vmop.numRebootGuest.latest": "cluster_vm_guest_ops_reboot",
207 + "vmop.numShutdownGuest.latest": "cluster_vm_guest_ops_shutdown",
208 + "vmop.numStandbyGuest.latest": "cluster_vm_guest_ops_standby",
209 + "vmop.numChangeDS.latest": "cluster_vm_cold_migrations_change_ds",
210 + "vmop.numChangeHost.latest": "cluster_vm_cold_migrations_change_host",
211 + "vmop.numChangeHostDS.latest": "cluster_vm_cold_migrations_change_host_ds",
212 +}
213 +
214 +const (
215 + recurringLogEvery = time.Hour
216
217 + logKeyHostNoPerfSamples = "vsphere:host-no-perf-samples"
218 + logKeyVMNoPerfSamples = "vsphere:vm-no-perf-samples"
219 + logKeyDatastorePropertyRefreshError = "vsphere:datastore-property-refresh-error"
220 + logKeyClusterPropertyRefreshError = "vsphere:cluster-property-refresh-error"
221 + logKeyResourcePoolRefreshError = "vsphere:resource-pool-property-refresh-error"
222 + logKeyDiscoveryError = "vsphere:periodic-discovery-error"
223 +)
224 +
225 +func (c *Collector) collectLocked() error {
226 c.Debug("starting collection process")
227 t := time.Now()
27 - mx := make(map[string]int64)
228 + c.hostPowerPerfSamples = nil
229 + c.vmPowerPerfSamples = nil
230 + c.vsanMetrics = nil
231
29 - err := c.collectHosts(mx)
232 + c.collectInventory()
233 +
234 + err := c.collectHosts()
235 if err != nil {
31 - return nil, err
236 + return fmt.Errorf("collect host metrics from vSphere resources: %w", err)
237 }
238
34 - err = c.collectVMs(mx)
239 + err = c.collectVMs()
240 if err != nil {
36 - return nil, err
241 + return fmt.Errorf("collect VM metrics from vSphere resources: %w", err)
242 }
243
39 - c.collectDatastores(mx)
40 - c.collectClusters(mx)
41 - c.collectResourcePools(mx)
42 -
43 - c.updateCharts()
244 + c.collectDatastores()
245 + c.collectClusters()
246 + c.collectResourcePools()
247 + c.collectVSAN()
248 + c.writeDatastoreClusterMetrics()
249 + c.writePowerMetrics()
250 + c.writeVSANMetrics()
251
252 c.Debugf("metrics collected, process took %s", time.Since(t))
253
47 - return mx, nil
254 + return nil
255 +}
256 +
257 +func (c *Collector) collectVSAN() {
258 + if !c.CollectVSAN || c.resources == nil {
259 + return
260 + }
261 + clusters, hosts, vms := c.vsanResources()
262 + c.vsanMetrics = c.ScrapeVSAN(clusters, hosts, vms)
263 +}
264 +
265 +func (c *Collector) collectInventory() {
266 + if c.resources == nil {
267 + return
268 + }
269 +
270 + labels := c.inventoryLabelSet()
271 + c.observeGauge("inventory_objects_datacenters", int64(len(c.resources.DataCenters)), labels)
272 + c.observeGauge("inventory_objects_folders", int64(len(c.resources.Folders)), labels)
273 + c.observeGauge("inventory_objects_clusters", int64(len(c.resources.Clusters)), labels)
274 + c.observeGauge("inventory_objects_hosts", int64(len(c.resources.Hosts)), labels)
275 + c.observeGauge("inventory_objects_vms", int64(len(c.resources.VMs)), labels)
276 + c.observeGauge("inventory_objects_datastores", int64(len(c.resources.Datastores)), labels)
277 + c.observeGauge("inventory_objects_resource_pools", int64(len(c.resources.ResourcePools)), labels)
278 }
279
50 -func (c *Collector) collectHosts(mx map[string]int64) error {
280 +func (c *Collector) collectHosts() error {
281 if len(c.resources.Hosts) == 0 {
282 return nil
283 }
284 + c.collectHostsPropertyMetrics()
285 +
286 + poweredOnHosts := numPoweredOnHosts(c.resources.Hosts)
287 + if poweredOnHosts == 0 {
288 + return nil
289 + }
290 +
291 // NOTE: returns unsorted if at least one types.PerfMetricId Instance is not ""
292 metrics := c.ScrapeHosts(c.resources.Hosts)
293 if len(metrics) == 0 {
57 - return errors.New("failed to scrape hosts metrics")
294 + c.Limit(logKeyHostNoPerfSamples, 1, recurringLogEvery).
295 + Warningf("collect host performance metrics: vSphere returned no samples for %d powered-on host(s) out of %d discovered host(s)", poweredOnHosts, len(c.resources.Hosts))
296 + return nil
297 }
298
60 - c.collectHostsMetrics(mx, metrics)
299 + c.collectHostsMetrics(metrics)
300
301 return nil
302 }
303
65 -func (c *Collector) collectHostsMetrics(mx map[string]int64, metrics []performance.EntityMetric) {
66 - for k := range c.discoveredHosts {
67 - c.discoveredHosts[k]++
304 +func (c *Collector) collectHostsPropertyMetrics() {
305 + for _, host := range c.resources.Hosts {
306 + c.writeHostPropertyMetrics(host)
307 }
308 +}
309
310 +func (c *Collector) collectHostsMetrics(metrics []performance.EntityMetric) {
311 for _, metric := range metrics {
312 if host := c.resources.Hosts.Get(metric.Entity.Value); host != nil {
72 - c.discoveredHosts[host.ID] = 0
73 - writeHostMetrics(mx, host, metric.Value)
313 + c.writeHostPerfMetrics(host, metric.Value)
314 + c.collectHostPowerMetrics(host, metric.Value)
315 }
316 }
317 }
318
78 -func writeHostMetrics(mx map[string]int64, host *rs.Host, metrics []performance.MetricSeries) {
319 +func numPoweredOnHosts(hosts rs.Hosts) (num int) {
320 + for _, host := range hosts {
321 + if host.IsPoweredOn() {
322 + num++
323 + }
324 + }
325 + return num
326 +}
327 +
328 +func (c *Collector) writeHostPerfMetrics(host *rs.Host, metrics []performance.MetricSeries) {
329 + labels := c.hostLabelSet(host)
330 for _, metric := range metrics {
331 + if _, ok := hostPowerMetricByCounter[metric.Name]; ok {
332 + continue
333 + }
334 + if metric.Instance != "" {
335 + continue
336 + }
337 if len(metric.Value) == 0 || metric.Value[0] == -1 {
338 continue
339 }
83 - key := fmt.Sprintf("%s_%s", host.ID, metric.Name)
84 - mx[key] = metric.Value[0]
340 + name := hostPerfMetricByCounter[metric.Name]
341 + if name == "" {
342 + continue
343 + }
344 + c.observeGauge(name, metric.Value[0], labels)
345 }
346 +}
347 +
348 +func (c *Collector) writeHostPropertyMetrics(host *rs.Host) {
349 + labels := c.hostLabelSet(host)
350 for _, v := range overallStatuses {
87 - key := fmt.Sprintf("%s_overall.status.%s", host.ID, v)
88 - mx[key] = oldmetrix.Bool(host.OverallStatus == v)
351 + c.observeGauge("host_overall_status_"+v, oldmetrix.Bool(host.OverallStatus == v), labels)
352 + }
353 + for _, v := range hostPowerStates {
354 + c.observeGauge("host_power_state_"+snakeStatus(v.key), oldmetrix.Bool(host.PowerState == v.value), labels)
355 + }
356 + for _, v := range hostConnectionStates {
357 + c.observeGauge("host_connection_state_"+snakeStatus(v.key), oldmetrix.Bool(host.ConnectionState == v.value), labels)
358 }
359 + c.observeGauge("host_maintenance_status_in_maintenance", oldmetrix.Bool(host.InMaintenanceMode), labels)
360 + c.observeGauge("host_maintenance_status_normal", oldmetrix.Bool(!host.InMaintenanceMode), labels)
361 }
362
92 -func (c *Collector) collectVMs(mx map[string]int64) error {
363 +func (c *Collector) collectVMs() error {
364 if len(c.resources.VMs) == 0 {
365 return nil
366 }
367 + c.collectVMsPropertyMetrics()
368 +
369 + poweredOnVMs := numPoweredOnVMs(c.resources.VMs)
370 + if poweredOnVMs == 0 {
371 + return nil
372 + }
373 +
374 // NOTE: returns unsorted if at least one types.PerfMetricId Instance is not ""
375 ems := c.ScrapeVMs(c.resources.VMs)
376 if len(ems) == 0 {
99 - return errors.New("failed to scrape vms metrics")
377 + c.Limit(logKeyVMNoPerfSamples, 1, recurringLogEvery).
378 + Warningf("collect VM performance metrics: vSphere returned no samples for %d powered-on VM(s) out of %d discovered VM(s)", poweredOnVMs, len(c.resources.VMs))
379 + return nil
380 }
381
102 - c.collectVMsMetrics(mx, ems)
382 + c.collectVMsMetrics(ems)
383
384 return nil
385 }
386
107 -func (c *Collector) collectVMsMetrics(mx map[string]int64, metrics []performance.EntityMetric) {
108 - for id := range c.discoveredVMs {
109 - c.discoveredVMs[id]++
387 +func (c *Collector) collectVMsPropertyMetrics() {
388 + for _, vm := range c.resources.VMs {
389 + c.writeVMPropertyMetrics(vm)
390 }
391 +}
392
393 +func (c *Collector) collectVMsMetrics(metrics []performance.EntityMetric) {
394 for _, metric := range metrics {
395 if vm := c.resources.VMs.Get(metric.Entity.Value); vm != nil {
114 - writeVMMetrics(mx, vm, metric.Value)
115 - c.discoveredVMs[vm.ID] = 0
396 + c.writeVMPerfMetrics(vm, metric.Value)
397 + c.collectVMPowerMetrics(vm, metric.Value)
398 + }
399 + }
400 +}
401 +
402 +func numPoweredOnVMs(vms rs.VMs) (num int) {
403 + for _, vm := range vms {
404 + if vm.IsPoweredOn() {
405 + num++
406 }
407 }
408 + return num
409 }
410
120 -func writeVMMetrics(mx map[string]int64, vm *rs.VM, metrics []performance.MetricSeries) {
411 +func (c *Collector) writeVMPerfMetrics(vm *rs.VM, metrics []performance.MetricSeries) {
412 + labels := c.vmLabelSet(vm)
413 for _, metric := range metrics {
414 + if _, ok := vmPowerMetricByCounter[metric.Name]; ok {
415 + continue
416 + }
417 + if metric.Instance != "" {
418 + continue
419 + }
420 if len(metric.Value) == 0 || metric.Value[0] == -1 {
421 continue
422 }
125 - key := fmt.Sprintf("%s_%s", vm.ID, metric.Name)
126 - mx[key] = metric.Value[0]
423 + name := vmPerfMetricByCounter[metric.Name]
424 + if name == "" {
425 + continue
426 + }
427 + c.observeGauge(name, metric.Value[0], labels)
428 }
429 +}
430 +
431 +func (c *Collector) writeVMPropertyMetrics(vm *rs.VM) {
432 + labels := c.vmLabelSet(vm)
433 for _, v := range overallStatuses {
129 - key := fmt.Sprintf("%s_overall.status.%s", vm.ID, v)
130 - mx[key] = oldmetrix.Bool(vm.OverallStatus == v)
434 + c.observeGauge("vm_overall_status_"+v, oldmetrix.Bool(vm.OverallStatus == v), labels)
435 + }
436 + for _, v := range vmPowerStates {
437 + c.observeGauge("vm_power_state_"+snakeStatus(v.key), oldmetrix.Bool(vm.PowerState == v.value), labels)
438 + }
439 + for _, v := range vmConnectionStates {
440 + c.observeGauge("vm_connection_state_"+snakeStatus(v.key), oldmetrix.Bool(vm.ConnectionState == v.value), labels)
441 + }
442 + toolsRunningStatusKnown := false
443 + for _, v := range vmToolsRunningStatuses {
444 + ok := vm.ToolsRunningStatus == v.value
445 + c.observeGauge("vm_tools_running_status_"+snakeStatus(v.key), oldmetrix.Bool(ok), labels)
446 + toolsRunningStatusKnown = toolsRunningStatusKnown || ok
447 + }
448 + c.observeGauge("vm_tools_running_status_unknown", oldmetrix.Bool(!toolsRunningStatusKnown), labels)
449 +
450 + toolsVersionStatusKnown := false
451 + for _, v := range vmToolsVersionStatuses {
452 + ok := vm.ToolsVersionStatus == v.value
453 + c.observeGauge("vm_tools_version_status_"+snakeStatus(v.key), oldmetrix.Bool(ok), labels)
454 + toolsVersionStatusKnown = toolsVersionStatusKnown || ok
455 + }
456 + c.observeGauge("vm_tools_version_status_unknown", oldmetrix.Bool(!toolsVersionStatusKnown), labels)
457 +
458 + c.observeGauge("vm_consolidation_needed_needed", oldmetrix.Bool(vm.ConsolidationNeeded), labels)
459 + c.observeGauge("vm_consolidation_needed_not_needed", oldmetrix.Bool(!vm.ConsolidationNeeded), labels)
460 + c.observeGauge("vm_config_cpu_vcpus", vm.ConfigCPU, labels)
461 + c.observeGauge("vm_config_memory_memory", vm.ConfigMemory, labels)
462 + c.observeGauge("vm_config_devices_disks", vm.ConfigDisks, labels)
463 + c.observeGauge("vm_config_devices_nics", vm.ConfigNICs, labels)
464 + c.observeGauge("vm_storage_usage_committed", vm.StorageCommitted, labels)
465 + c.observeGauge("vm_storage_usage_uncommitted", vm.StorageUncommitted, labels)
466 + c.observeGauge("vm_storage_usage_unshared", vm.StorageUnshared, labels)
467 + c.observeGauge("vm_snapshot_count_count", vm.SnapshotCount, labels)
468 + c.observeGauge("vm_snapshot_max_chain_depth_depth", vm.SnapshotMaxChainDepth, labels)
469 + c.observeGauge("vm_snapshot_max_age_age", snapshotMaxAgeSeconds(vm.SnapshotOldestCreateTime), labels)
470 +}
471 +
472 +func snapshotMaxAgeSeconds(oldest time.Time) int64 {
473 + if oldest.IsZero() {
474 + return 0
475 + }
476 + age := time.Since(oldest).Seconds()
477 + if age < 0 {
478 + return 0
479 }
480 + return int64(age)
481 }
482
134 -func (c *Collector) collectDatastores(mx map[string]int64) {
483 +func snakeStatus(s string) string {
484 + switch s {
485 + // Chart selectors use the VMware UI spelling, not the enum's camel-case suffix.
486 + case "standBy":
487 + return "standby"
488 + // The chart dimension is "disabled"; the enum name includes the monitored subsystem.
489 + case "vmMonitoringDisabled":
490 + return "disabled"
491 + }
492 +
493 + var b strings.Builder
494 + for i, r := range s {
495 + if unicode.IsUpper(r) {
496 + if i > 0 {
497 + b.WriteByte('_')
498 + }
499 + r = unicode.ToLower(r)
500 + }
501 + b.WriteRune(r)
502 + }
503 + return b.String()
504 +}
505 +
506 +func (c *Collector) collectDatastores() {
507 if len(c.resources.Datastores) == 0 {
508 return
509 }
@@ -141,7 +513,7 @@ func (c *Collector) collectDatastores(mx map[string]int64) {
513 metrics := c.ScrapeDatastores(c.resources.Datastores)
514 // Datastore perf counters may return empty for vSAN or when no historical data is available yet.
515 // This is not an error — we still collect capacity and status from properties.
144 - c.collectDatastoresMetrics(mx, metrics, refreshed)
516 + c.collectDatastoresMetrics(metrics, refreshed)
517 }
518
519 func (c *Collector) refreshDatastoreProperties() map[string]bool {
@@ -156,9 +528,11 @@ func (c *Collector) refreshDatastoreProperties() map[string]bool {
528 refs = append(refs, ds.Ref)
529 }
530
159 - dsList, err := c.dsPropertyCollector.DatastoresByRef(refs, "summary", "overallStatus")
531 + pathSet := []string{"summary", "overallStatus"}
532 + dsList, err := c.dsPropertyCollector.DatastoresByRef(refs, pathSet...)
533 if err != nil {
161 - c.Warningf("failed to refresh datastore properties: %v", err)
534 + c.Limit(logKeyDatastorePropertyRefreshError, 1, recurringLogEvery).
535 + Warningf("collect vSphere datastore properties refresh: refs=%d pathSet=%v: %v", len(refs), pathSet, err)
536 return refreshed
537 }
538
@@ -168,73 +542,93 @@ func (c *Collector) refreshDatastoreProperties() map[string]bool {
542 continue
543 }
544 refreshed[ds.ID] = true
545 + ds.Type = raw.Summary.Type
546 ds.Capacity = raw.Summary.Capacity
547 ds.FreeSpace = raw.Summary.FreeSpace
548 + ds.Uncommitted = raw.Summary.Uncommitted
549 ds.Accessible = raw.Summary.Accessible
550 + ds.MaintenanceMode = raw.Summary.MaintenanceMode
551 + ds.MultipleHostAccess = raw.Summary.MultipleHostAccess
552 ds.OverallStatus = string(raw.OverallStatus)
553 }
554
555 return refreshed
556 }
557
180 -func (c *Collector) collectDatastoresMetrics(mx map[string]int64, metrics []performance.EntityMetric, refreshed map[string]bool) {
181 - for id := range c.discoveredDatastores {
182 - c.discoveredDatastores[id]++
183 - }
184 -
558 +func (c *Collector) collectDatastoresMetrics(metrics []performance.EntityMetric, refreshed map[string]bool) {
559 + // Property metrics reflect cached resource fields, so emit them only after
560 + // the current refresh succeeds. Perf samples are fetched separately.
561 for _, ds := range c.resources.Datastores {
562 if refreshed[ds.ID] {
187 - c.discoveredDatastores[ds.ID] = 0
563 + c.writeDatastoreMetrics(ds)
564 }
189 - writeDatastoreMetrics(mx, ds)
565 }
566
567 for _, metric := range metrics {
568 if ds := c.resources.Datastores.Get(metric.Entity.Value); ds != nil {
194 - c.discoveredDatastores[ds.ID] = 0
195 - c.datastorePerfReceived[ds.ID] = true
196 - writeDatastorePerfMetrics(mx, ds, metric.Value)
569 + c.writeDatastorePerfMetrics(ds, metric.Value)
570 }
571 }
572 }
573
201 -func writeDatastoreMetrics(mx map[string]int64, ds *rs.Datastore) {
574 +func (c *Collector) writeDatastoreMetrics(ds *rs.Datastore) {
575 // VMware docs: Capacity and FreeSpace are guaranteed valid only when Accessible is true.
203 - var capacity, freeSpace, used int64
576 + var capacity, freeSpace, used, uncommitted int64
577 if ds.Accessible {
578 capacity = ds.Capacity
579 freeSpace = ds.FreeSpace
580 used = max(capacity-freeSpace, 0)
581 + uncommitted = ds.Uncommitted
582 }
583
210 - mx[fmt.Sprintf("%s_capacity", ds.ID)] = capacity
211 - mx[fmt.Sprintf("%s_free_space", ds.ID)] = freeSpace
212 - mx[fmt.Sprintf("%s_used_space", ds.ID)] = used
584 + labels := c.datastoreLabelSet(ds)
585 + c.observeGauge("datastore_space_usage_capacity", capacity, labels)
586 + c.observeGauge("datastore_space_usage_free", freeSpace, labels)
587 + c.observeGauge("datastore_space_usage_used", used, labels)
588 + c.observeGauge("datastore_space_usage_uncommitted", uncommitted, labels)
589
590 if capacity > 0 {
591 // use float64 to avoid int64 overflow on datastores larger than 922 TB
216 - mx[fmt.Sprintf("%s_used_space_pct", ds.ID)] = int64(float64(used) / float64(capacity) * 10000)
592 + c.observeGauge("datastore_space_utilization_used", int64(float64(used)/float64(capacity)*scaledPercent), labels)
593 } else {
218 - mx[fmt.Sprintf("%s_used_space_pct", ds.ID)] = 0
594 + c.observeGauge("datastore_space_utilization_used", 0, labels)
595 }
596
597 for _, v := range overallStatuses {
222 - key := fmt.Sprintf("%s_overall.status.%s", ds.ID, v)
223 - mx[key] = oldmetrix.Bool(ds.OverallStatus == v)
598 + c.observeGauge("datastore_overall_status_"+v, oldmetrix.Bool(ds.OverallStatus == v), labels)
599 + }
600 +
601 + c.observeGauge("datastore_accessibility_status_accessible", oldmetrix.Bool(ds.Accessible), labels)
602 + c.observeGauge("datastore_accessibility_status_inaccessible", oldmetrix.Bool(!ds.Accessible), labels)
603 +
604 + maintenanceModeKnown := false
605 + for _, mode := range datastoreMaintenanceModes {
606 + ok := ds.MaintenanceMode == mode.value
607 + maintenanceModeKnown = maintenanceModeKnown || ok
608 + c.observeGauge("datastore_maintenance_status_"+snakeStatus(mode.key), oldmetrix.Bool(ok), labels)
609 }
610 + c.observeGauge("datastore_maintenance_status_unknown", oldmetrix.Bool(!maintenanceModeKnown), labels)
611 +
612 + c.observeGauge("datastore_multiple_host_access_enabled", oldmetrix.Bool(ds.MultipleHostAccess != nil && *ds.MultipleHostAccess), labels)
613 + c.observeGauge("datastore_multiple_host_access_disabled", oldmetrix.Bool(ds.MultipleHostAccess != nil && !*ds.MultipleHostAccess), labels)
614 + c.observeGauge("datastore_multiple_host_access_unknown", oldmetrix.Bool(ds.MultipleHostAccess == nil), labels)
615 }
616
227 -func writeDatastorePerfMetrics(mx map[string]int64, ds *rs.Datastore, metrics []performance.MetricSeries) {
617 +func (c *Collector) writeDatastorePerfMetrics(ds *rs.Datastore, metrics []performance.MetricSeries) {
618 + labels := c.datastoreLabelSet(ds)
619 for _, metric := range metrics {
620 if len(metric.Value) == 0 || metric.Value[0] == -1 {
621 continue
622 }
232 - key := fmt.Sprintf("%s_%s", ds.ID, metric.Name)
233 - mx[key] = metric.Value[0]
623 + name := datastorePerfMetricByCounter[metric.Name]
624 + if name == "" {
625 + continue
626 + }
627 + c.observeGauge(name, metric.Value[0], labels)
628 }
629 }
630
237 -func (c *Collector) collectClusters(mx map[string]int64) {
631 +func (c *Collector) collectClusters() {
632 if len(c.resources.Clusters) == 0 {
633 return
634 }
@@ -242,7 +636,7 @@ func (c *Collector) collectClusters(mx map[string]int64) {
636 refreshed := c.refreshClusterProperties()
637
638 metrics := c.ScrapeClusters(c.resources.Clusters)
245 - c.collectClustersMetrics(mx, metrics, refreshed)
639 + c.collectClustersMetrics(metrics, refreshed)
640 }
641
642 func (c *Collector) refreshClusterProperties() map[string]bool {
@@ -257,9 +651,11 @@ func (c *Collector) refreshClusterProperties() map[string]bool {
651 refs = append(refs, cl.Ref)
652 }
653
260 - clusters, err := c.clusterPropertyCollector.ClustersByRef(refs, "name", "summary", "configurationEx", "overallStatus")
654 + pathSet := []string{"name", "summary", "configurationEx", "overallStatus"}
655 + clusters, err := c.clusterPropertyCollector.ClustersByRef(refs, pathSet...)
656 if err != nil {
262 - c.Warningf("failed to refresh cluster properties: %v", err)
657 + c.Limit(logKeyClusterPropertyRefreshError, 1, recurringLogEvery).
658 + Warningf("collect vSphere cluster properties refresh: refs=%d pathSet=%v: %v", len(refs), pathSet, err)
659 return refreshed
660 }
661
@@ -308,8 +704,12 @@ func updateClusterFromProperties(cl *rs.Cluster, raw mo.ClusterComputeResource)
704 cl.UsagePoweredOffVmCount = 0
705 cl.DrsEnabled = false
706 cl.DrsMode = ""
707 + cl.DrsVmotionRate = 0
708 cl.HaEnabled = false
709 cl.HaAdmCtrlEnabled = false
710 + cl.HaHostMonitoring = ""
711 + cl.HaVMMonitoring = ""
712 + cl.HaVMComponentProtection = ""
713
714 // Cluster-specific summary fields
715 if cs, ok := raw.Summary.(*types.ClusterComputeResourceSummary); ok {
@@ -331,92 +731,128 @@ func updateClusterFromProperties(cl *rs.Cluster, raw mo.ClusterComputeResource)
731
732 // DRS and HA config from configurationEx
733 if cfg, ok := raw.ConfigurationEx.(*types.ClusterConfigInfoEx); ok {
734 + rs.SetClusterVSANInfo(cl, cfg)
735 if cfg.DrsConfig.Enabled != nil {
736 cl.DrsEnabled = *cfg.DrsConfig.Enabled
737 }
738 cl.DrsMode = string(cfg.DrsConfig.DefaultVmBehavior)
739 + cl.DrsVmotionRate = cfg.DrsConfig.VmotionRate
740 if cfg.DasConfig.Enabled != nil {
741 cl.HaEnabled = *cfg.DasConfig.Enabled
742 }
743 if cfg.DasConfig.AdmissionControlEnabled != nil {
744 cl.HaAdmCtrlEnabled = *cfg.DasConfig.AdmissionControlEnabled
745 }
746 + cl.HaHostMonitoring = cfg.DasConfig.HostMonitoring
747 + cl.HaVMMonitoring = cfg.DasConfig.VmMonitoring
748 + cl.HaVMComponentProtection = cfg.DasConfig.VmComponentProtecting
749 }
750 }
751
347 -func (c *Collector) collectClustersMetrics(mx map[string]int64, metrics []performance.EntityMetric, refreshed map[string]bool) {
348 - for id := range c.discoveredClusters {
349 - c.discoveredClusters[id]++
350 - }
351 -
752 +func (c *Collector) collectClustersMetrics(metrics []performance.EntityMetric, refreshed map[string]bool) {
753 + // Property metrics reflect cached resource fields, so emit them only after
754 + // the current refresh succeeds. Perf samples are fetched separately.
755 for _, cl := range c.resources.Clusters {
756 if refreshed[cl.ID] {
354 - c.discoveredClusters[cl.ID] = 0
757 + c.writeClusterPropertyMetrics(cl)
758 }
356 - writeClusterPropertyMetrics(mx, cl)
759 }
760
761 for _, metric := range metrics {
762 if cl := c.resources.Clusters.Get(metric.Entity.Value); cl != nil {
361 - c.discoveredClusters[cl.ID] = 0
362 - c.clusterPerfReceived[cl.ID] = true
363 - writeClusterPerfMetrics(mx, cl, metric.Value)
763 + c.writeClusterPerfMetrics(cl, metric.Value)
764 }
765 }
766 }
767
368 -func writeClusterPropertyMetrics(mx map[string]int64, cl *rs.Cluster) {
369 - mx[fmt.Sprintf("%s_num_hosts", cl.ID)] = int64(cl.NumHosts)
370 - mx[fmt.Sprintf("%s_num_effective_hosts", cl.ID)] = int64(cl.NumEffectiveHosts)
371 - mx[fmt.Sprintf("%s_total_cpu", cl.ID)] = int64(cl.TotalCpu)
372 - mx[fmt.Sprintf("%s_effective_cpu", cl.ID)] = int64(cl.EffectiveCpu)
373 - mx[fmt.Sprintf("%s_total_memory", cl.ID)] = cl.TotalMemory
768 +func (c *Collector) writeClusterPropertyMetrics(cl *rs.Cluster) {
769 + labels := c.clusterLabelSet(cl)
770 + c.observeGauge("cluster_hosts_total", int64(cl.NumHosts), labels)
771 + c.observeGauge("cluster_hosts_effective", int64(cl.NumEffectiveHosts), labels)
772 + c.observeGauge("cluster_cpu_capacity_total", int64(cl.TotalCpu), labels)
773 + c.observeGauge("cluster_cpu_capacity_effective", int64(cl.EffectiveCpu), labels)
774 + c.observeGauge("cluster_mem_capacity_total", cl.TotalMemory, labels)
775 // EffectiveMemory is MB from API, convert to bytes for consistency with TotalMemory
375 - mx[fmt.Sprintf("%s_effective_memory", cl.ID)] = cl.EffectiveMemory * 1024 * 1024
376 - mx[fmt.Sprintf("%s_num_cpu_cores", cl.ID)] = int64(cl.NumCpuCores)
377 - mx[fmt.Sprintf("%s_num_cpu_threads", cl.ID)] = int64(cl.NumCpuThreads)
378 - mx[fmt.Sprintf("%s_num_vmotions", cl.ID)] = int64(cl.NumVmotions)
379 - mx[fmt.Sprintf("%s_drs_score", cl.ID)] = int64(cl.DrsScore)
380 - mx[fmt.Sprintf("%s_current_balance", cl.ID)] = int64(cl.CurrentBalance)
381 - mx[fmt.Sprintf("%s_target_balance", cl.ID)] = int64(cl.TargetBalance)
382 -
383 - mx[fmt.Sprintf("%s_drs_enabled", cl.ID)] = oldmetrix.Bool(cl.DrsEnabled)
384 - mx[fmt.Sprintf("%s_ha_enabled", cl.ID)] = oldmetrix.Bool(cl.HaEnabled)
385 - mx[fmt.Sprintf("%s_ha_adm_ctrl_enabled", cl.ID)] = oldmetrix.Bool(cl.HaAdmCtrlEnabled)
386 -
387 - mx[fmt.Sprintf("%s_usage_cpu_demand_mhz", cl.ID)] = int64(cl.UsageCpuDemandMhz)
388 - mx[fmt.Sprintf("%s_usage_mem_demand_mb", cl.ID)] = int64(cl.UsageMemDemandMB)
389 - mx[fmt.Sprintf("%s_usage_cpu_entitled_mhz", cl.ID)] = int64(cl.UsageCpuEntitledMhz)
390 - mx[fmt.Sprintf("%s_usage_mem_entitled_mb", cl.ID)] = int64(cl.UsageMemEntitledMB)
391 - mx[fmt.Sprintf("%s_usage_cpu_reservation_mhz", cl.ID)] = int64(cl.UsageCpuReservationMhz)
392 - mx[fmt.Sprintf("%s_usage_mem_reservation_mb", cl.ID)] = int64(cl.UsageMemReservationMB)
393 - mx[fmt.Sprintf("%s_usage_total_vm_count", cl.ID)] = int64(cl.UsageTotalVmCount)
394 - mx[fmt.Sprintf("%s_usage_powered_off_vm_count", cl.ID)] = int64(cl.UsagePoweredOffVmCount)
776 + c.observeGauge("cluster_mem_capacity_effective", cl.EffectiveMemory*1024*1024, labels)
777 + c.observeGauge("cluster_cpu_topology_cores", int64(cl.NumCpuCores), labels)
778 + c.observeGauge("cluster_cpu_topology_threads", int64(cl.NumCpuThreads), labels)
779 + c.observeGauge("cluster_vmotions_vmotions", int64(cl.NumVmotions), labels)
780 + c.observeGauge("cluster_drs_score_score", int64(cl.DrsScore), labels)
781 + c.observeGauge("cluster_drs_balance_current", int64(cl.CurrentBalance), labels)
782 + c.observeGauge("cluster_drs_balance_target", int64(cl.TargetBalance), labels)
783 +
784 + c.observeGauge("cluster_drs_config_enabled", oldmetrix.Bool(cl.DrsEnabled), labels)
785 + drsModeKnown := false
786 + for _, v := range clusterDRSModes {
787 + ok := cl.DrsMode == v.value
788 + c.observeGauge("cluster_drs_mode_"+snakeStatus(v.key), oldmetrix.Bool(ok), labels)
789 + drsModeKnown = drsModeKnown || ok
790 + }
791 + c.observeGauge("cluster_drs_mode_unknown", oldmetrix.Bool(!drsModeKnown), labels)
792 + c.observeGauge("cluster_drs_vmotion_rate_rate", int64(cl.DrsVmotionRate), labels)
793 +
794 + c.observeGauge("cluster_ha_config_enabled", oldmetrix.Bool(cl.HaEnabled), labels)
795 + c.observeGauge("cluster_ha_config_admission_control", oldmetrix.Bool(cl.HaAdmCtrlEnabled), labels)
796 + c.writeClusterHAServiceState("cluster_ha_host_monitoring", cl.HaHostMonitoring, labels)
797 + c.writeClusterHAVMMonitoringState(cl.HaVMMonitoring, labels)
798 + c.writeClusterHAServiceState("cluster_ha_vm_component_protection", cl.HaVMComponentProtection, labels)
799 +
800 + c.observeGauge("cluster_usage_cpu_demand", int64(cl.UsageCpuDemandMhz), labels)
801 + c.observeGauge("cluster_usage_mem_demand", int64(cl.UsageMemDemandMB), labels)
802 + c.observeGauge("cluster_usage_cpu_entitled", int64(cl.UsageCpuEntitledMhz), labels)
803 + c.observeGauge("cluster_usage_mem_entitled", int64(cl.UsageMemEntitledMB), labels)
804 + c.observeGauge("cluster_usage_cpu_reserved", int64(cl.UsageCpuReservationMhz), labels)
805 + c.observeGauge("cluster_usage_mem_reserved", int64(cl.UsageMemReservationMB), labels)
806 + c.observeGauge("cluster_vm_count_total", int64(cl.UsageTotalVmCount), labels)
807 + c.observeGauge("cluster_vm_count_powered_off", int64(cl.UsagePoweredOffVmCount), labels)
808
809 for _, v := range overallStatuses {
397 - key := fmt.Sprintf("%s_overall.status.%s", cl.ID, v)
398 - mx[key] = oldmetrix.Bool(cl.OverallStatus == v)
810 + c.observeGauge("cluster_overall_status_"+v, oldmetrix.Bool(cl.OverallStatus == v), labels)
811 }
812 }
813
402 -func writeClusterPerfMetrics(mx map[string]int64, cl *rs.Cluster, metrics []performance.MetricSeries) {
814 +func (c *Collector) writeClusterHAServiceState(prefix, state string, labels metrix.LabelSet) {
815 + known := false
816 + for _, v := range clusterHAServiceStates {
817 + ok := state == v.value
818 + c.observeGauge(prefix+"_"+snakeStatus(v.key), oldmetrix.Bool(ok), labels)
819 + known = known || ok
820 + }
821 + c.observeGauge(prefix+"_unknown", oldmetrix.Bool(!known), labels)
822 +}
823 +
824 +func (c *Collector) writeClusterHAVMMonitoringState(state string, labels metrix.LabelSet) {
825 + known := false
826 + for _, v := range clusterHAVMMonitoringStates {
827 + ok := state == v.value
828 + c.observeGauge("cluster_ha_vm_monitoring_"+snakeStatus(v.key), oldmetrix.Bool(ok), labels)
829 + known = known || ok
830 + }
831 + c.observeGauge("cluster_ha_vm_monitoring_unknown", oldmetrix.Bool(!known), labels)
832 +}
833 +
834 +func (c *Collector) writeClusterPerfMetrics(cl *rs.Cluster, metrics []performance.MetricSeries) {
835 + labels := c.clusterLabelSet(cl)
836 for _, metric := range metrics {
837 if len(metric.Value) == 0 || metric.Value[0] == -1 {
838 continue
839 }
407 - key := fmt.Sprintf("%s_%s", cl.ID, metric.Name)
408 - mx[key] = metric.Value[0]
840 + name := clusterPerfMetricByCounter[metric.Name]
841 + if name == "" {
842 + continue
843 + }
844 + c.observeGauge(name, metric.Value[0], labels)
845 }
846 }
847
412 -func (c *Collector) collectResourcePools(mx map[string]int64) {
848 +func (c *Collector) collectResourcePools() {
849 if len(c.resources.ResourcePools) == 0 {
850 return
851 }
852
853 refreshed := c.refreshResourcePoolProperties()
854
419 - c.collectResourcePoolsMetrics(mx, refreshed)
855 + c.collectResourcePoolsMetrics(refreshed)
856 }
857
858 func (c *Collector) refreshResourcePoolProperties() map[string]bool {
@@ -431,9 +867,11 @@ func (c *Collector) refreshResourcePoolProperties() map[string]bool {
867 refs = append(refs, rp.Ref)
868 }
869
434 - pools, err := c.rpPropertyCollector.ResourcePoolsByRef(refs, "name", "summary", "config", "runtime", "overallStatus")
870 + pathSet := []string{"name", "summary", "config", "runtime", "overallStatus"}
871 + pools, err := c.rpPropertyCollector.ResourcePoolsByRef(refs, pathSet...)
872 if err != nil {
436 - c.Warningf("failed to refresh resource pool properties: %v", err)
873 + c.Limit(logKeyResourcePoolRefreshError, 1, recurringLogEvery).
874 + Warningf("collect vSphere resource pool properties refresh: refs=%d pathSet=%v: %v", len(refs), pathSet, err)
875 return refreshed
876 }
877
@@ -489,7 +927,8 @@ func updateResourcePoolFromProperties(rp *rs.ResourcePool, raw mo.ResourcePool)
927 rp.CompressedMemory = qs.CompressedMemory
928 }
929
492 - // Runtime resource usage (full "runtime" property requested)
930 + // Runtime and Config are value structs in mo.ResourcePool; missing properties
931 + // decode as zero values rather than nil pointers.
932 rp.CpuReservationUsed = raw.Runtime.Cpu.ReservationUsed
933 rp.CpuMaxUsage = raw.Runtime.Cpu.MaxUsage
934 rp.CpuUnreservedForVm = raw.Runtime.Cpu.UnreservedForVm
@@ -518,49 +957,47 @@ func updateResourcePoolFromProperties(rp *rs.ResourcePool, raw mo.ResourcePool)
957 // CpuSharesLevel / MemSharesLevel are strings (low/normal/high/custom) — not exported as metrics
958 }
959
521 -func (c *Collector) collectResourcePoolsMetrics(mx map[string]int64, refreshed map[string]bool) {
522 - for id := range c.discoveredResourcePools {
523 - c.discoveredResourcePools[id]++
524 - }
525 -
960 +func (c *Collector) collectResourcePoolsMetrics(refreshed map[string]bool) {
961 + // Property metrics reflect cached resource fields, so emit them only after
962 + // the current refresh succeeds.
963 for _, rp := range c.resources.ResourcePools {
964 if refreshed[rp.ID] {
528 - c.discoveredResourcePools[rp.ID] = 0
965 + c.writeResourcePoolMetrics(rp)
966 }
530 - writeResourcePoolMetrics(mx, rp)
531 - }
532 -}
533 -
534 -func writeResourcePoolMetrics(mx map[string]int64, rp *rs.ResourcePool) {
535 - mx[fmt.Sprintf("%s_cpu_usage", rp.ID)] = rp.OverallCpuUsage
536 - mx[fmt.Sprintf("%s_cpu_demand", rp.ID)] = rp.OverallCpuDemand
537 - mx[fmt.Sprintf("%s_cpu_entitlement_distributed", rp.ID)] = rp.DistributedCpuEntitlement
538 - mx[fmt.Sprintf("%s_mem_usage_guest", rp.ID)] = rp.GuestMemoryUsage
539 - mx[fmt.Sprintf("%s_mem_usage_host", rp.ID)] = rp.HostMemoryUsage
540 - mx[fmt.Sprintf("%s_mem_entitlement_distributed", rp.ID)] = rp.DistributedMemoryEntitlement
541 -
542 - mx[fmt.Sprintf("%s_mem_private", rp.ID)] = rp.PrivateMemory
543 - mx[fmt.Sprintf("%s_mem_shared", rp.ID)] = rp.SharedMemory
544 - mx[fmt.Sprintf("%s_mem_swapped", rp.ID)] = rp.SwappedMemory
545 - mx[fmt.Sprintf("%s_mem_ballooned", rp.ID)] = rp.BalloonedMemory
546 - mx[fmt.Sprintf("%s_mem_overhead", rp.ID)] = rp.OverheadMemory
547 - mx[fmt.Sprintf("%s_mem_consumed_overhead", rp.ID)] = rp.ConsumedOverheadMemory
548 - mx[fmt.Sprintf("%s_mem_compressed", rp.ID)] = rp.CompressedMemory
549 -
550 - mx[fmt.Sprintf("%s_cpu_reservation_used", rp.ID)] = rp.CpuReservationUsed
551 - mx[fmt.Sprintf("%s_cpu_max_usage", rp.ID)] = rp.CpuMaxUsage
552 - mx[fmt.Sprintf("%s_cpu_unreserved_for_vm", rp.ID)] = rp.CpuUnreservedForVm
553 - mx[fmt.Sprintf("%s_mem_reservation_used", rp.ID)] = rp.MemReservationUsed
554 - mx[fmt.Sprintf("%s_mem_max_usage", rp.ID)] = rp.MemMaxUsage
555 - mx[fmt.Sprintf("%s_mem_unreserved_for_vm", rp.ID)] = rp.MemUnreservedForVm
556 -
557 - mx[fmt.Sprintf("%s_cpu_reservation", rp.ID)] = rp.CpuReservation
558 - mx[fmt.Sprintf("%s_cpu_limit", rp.ID)] = rp.CpuLimit
559 - mx[fmt.Sprintf("%s_mem_reservation", rp.ID)] = rp.MemReservation
560 - mx[fmt.Sprintf("%s_mem_limit", rp.ID)] = rp.MemLimit
967 + }
968 +}
969 +
970 +func (c *Collector) writeResourcePoolMetrics(rp *rs.ResourcePool) {
971 + labels := c.resourcePoolLabelSet(rp)
972 + c.observeGauge("resource_pool_cpu_usage_usage", rp.OverallCpuUsage, labels)
973 + c.observeGauge("resource_pool_cpu_usage_demand", rp.OverallCpuDemand, labels)
974 + c.observeGauge("resource_pool_cpu_entitlement_distributed", rp.DistributedCpuEntitlement, labels)
975 + c.observeGauge("resource_pool_mem_usage_guest", rp.GuestMemoryUsage, labels)
976 + c.observeGauge("resource_pool_mem_usage_host", rp.HostMemoryUsage, labels)
977 + c.observeGauge("resource_pool_mem_entitlement_distributed", rp.DistributedMemoryEntitlement, labels)
978 +
979 + c.observeGauge("resource_pool_mem_breakdown_private", rp.PrivateMemory, labels)
980 + c.observeGauge("resource_pool_mem_breakdown_shared", rp.SharedMemory, labels)
981 + c.observeGauge("resource_pool_mem_breakdown_swapped", rp.SwappedMemory, labels)
982 + c.observeGauge("resource_pool_mem_breakdown_ballooned", rp.BalloonedMemory, labels)
983 + c.observeGauge("resource_pool_mem_breakdown_overhead", rp.OverheadMemory, labels)
984 + c.observeGauge("resource_pool_mem_breakdown_consumed_overhead", rp.ConsumedOverheadMemory, labels)
985 + // vSphere reports CompressedMemory in KiB; the chart keeps V1's MB display scale.
986 + c.observeGauge("resource_pool_mem_breakdown_compressed", rp.CompressedMemory, labels)
987 +
988 + c.observeGauge("resource_pool_cpu_allocation_reservation_used", rp.CpuReservationUsed, labels)
989 + c.observeGauge("resource_pool_cpu_allocation_max_usage", rp.CpuMaxUsage, labels)
990 + c.observeGauge("resource_pool_cpu_allocation_unreserved_for_vm", rp.CpuUnreservedForVm, labels)
991 + c.observeGauge("resource_pool_mem_allocation_reservation_used", rp.MemReservationUsed, labels)
992 + c.observeGauge("resource_pool_mem_allocation_max_usage", rp.MemMaxUsage, labels)
993 + c.observeGauge("resource_pool_mem_allocation_unreserved_for_vm", rp.MemUnreservedForVm, labels)
994 +
995 + c.observeGauge("resource_pool_cpu_config_reservation", rp.CpuReservation, labels)
996 + c.observeGauge("resource_pool_cpu_config_limit", rp.CpuLimit, labels)
997 + c.observeGauge("resource_pool_mem_config_reservation", rp.MemReservation, labels)
998 + c.observeGauge("resource_pool_mem_config_limit", rp.MemLimit, labels)
999
1000 for _, v := range overallStatuses {
563 - key := fmt.Sprintf("%s_overall.status.%s", rp.ID, v)
564 - mx[key] = oldmetrix.Bool(rp.OverallStatus == v)
1001 + c.observeGauge("resource_pool_overall_status_"+v, oldmetrix.Bool(rp.OverallStatus == v), labels)
1002 }
1003 }
src/go/plugin/go.d/collector/vsphere/collector.go
+136 -54
@@ -14,27 +14,39 @@ import (
14 "github.com/vmware/govmomi/vim25/types"
15
16 "github.com/netdata/netdata/go/plugins/pkg/confopt"
17 + "github.com/netdata/netdata/go/plugins/pkg/matcher"
18 + "github.com/netdata/netdata/go/plugins/pkg/metrix"
19 "github.com/netdata/netdata/go/plugins/pkg/web"
20 "github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi"
21 + clientpkg "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/client"
22 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/match"
23 rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
24 + scrapepkg "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/scrape"
25 )
26
27 //go:embed "config_schema.json"
28 var configSchema string
29
30 +//go:embed "charts.yaml"
31 +var chartTemplateYAML string
32 +
33 func init() {
34 collectorapi.Register("vsphere", collectorapi.Creator{
35 JobConfigSchema: configSchema,
36 Defaults: collectorapi.Defaults{
37 UpdateEvery: 20,
38 },
32 - Create: func() collectorapi.CollectorV1 { return New() },
33 - Config: func() any { return &Config{} },
39 + CreateV2: func() collectorapi.CollectorV2 { return New() },
40 + Config: func() any { return &Config{} },
41 + Methods: vsphereMethods,
42 + MethodHandler: vsphereMethodHandler,
43 })
44 }
45
46 func New() *Collector {
47 + store := metrix.NewCollectorStore()
48 + mx := newCollectorMetrics(store)
49 +
50 return &Collector{
51 Config: Config{
52 HTTPConfig: web.HTTPConfig{
@@ -42,37 +54,49 @@ func New() *Collector {
54 Timeout: confopt.Duration(time.Second * 20),
55 },
56 },
45 - DiscoveryInterval: confopt.Duration(time.Minute * 5),
46 - HostsInclude: []string{"/*"},
47 - VMsInclude: []string{"/*"},
48 - DatastoresInclude: []string{"/*"},
49 - ClustersInclude: []string{"/*"},
57 + DiscoveryInterval: confopt.Duration(time.Minute * 5),
58 + HostsInclude: match.HostIncludes{"/*"},
59 + VMsInclude: match.VMIncludes{"/*"},
60 + DatastoresInclude: match.DatastoreIncludes{"/*"},
61 + ClustersInclude: match.ClusterIncludes{"/*"},
62 + DatastoreClustersInclude: match.DatastoreClusterIncludes{"/*"},
63 + CollectVSAN: false,
64 + VSANClustersInclude: match.VSANClusterIncludes{"/*"},
65 + VSANHostsInclude: match.VSANHostIncludes{"/*"},
66 + VSANVMsInclude: match.VSANVMIncludes{"/*"},
67 },
51 - collectionLock: &sync.RWMutex{},
52 - charts: &collectorapi.Charts{},
53 - discoveredHosts: make(map[string]int),
54 - discoveredVMs: make(map[string]int),
55 - discoveredDatastores: make(map[string]int),
56 - discoveredClusters: make(map[string]int),
57 - discoveredResourcePools: make(map[string]int),
58 - charted: make(map[string]bool),
59 - datastorePerfReceived: make(map[string]bool),
60 - datastorePerfCharted: make(map[string]bool),
61 - clusterPerfReceived: make(map[string]bool),
62 - clusterPerfCharted: make(map[string]bool),
68 + store: store,
69 + mx: mx,
70 + collectionLock: &sync.RWMutex{},
71 }
72 }
73
74 type Config struct {
75 + // Job identity and scheduling.
76 Vnode string `yaml:"vnode,omitempty" json:"vnode"`
77 UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
78 AutoDetectionRetry int `yaml:"autodetection_retry,omitempty" json:"autodetection_retry"`
79 web.HTTPConfig `yaml:",inline" json:""`
71 - DiscoveryInterval confopt.Duration `yaml:"discovery_interval,omitempty" json:"discovery_interval"`
72 - HostsInclude match.HostIncludes `yaml:"host_include,omitempty" json:"host_include"`
73 - VMsInclude match.VMIncludes `yaml:"vm_include,omitempty" json:"vm_include"`
74 - DatastoresInclude match.DatastoreIncludes `yaml:"datastore_include,omitempty" json:"datastore_include"`
75 - ClustersInclude match.ClusterIncludes `yaml:"cluster_include,omitempty" json:"cluster_include"`
80 +
81 + // Inventory discovery and resource selectors.
82 + DiscoveryInterval confopt.Duration `yaml:"discovery_interval,omitempty" json:"discovery_interval"`
83 + HostsInclude match.HostIncludes `yaml:"host_include,omitempty" json:"host_include"`
84 + VMsInclude match.VMIncludes `yaml:"vm_include,omitempty" json:"vm_include"`
85 + DatastoresInclude match.DatastoreIncludes `yaml:"datastore_include,omitempty" json:"datastore_include"`
86 + ClustersInclude match.ClusterIncludes `yaml:"cluster_include,omitempty" json:"cluster_include"`
87 + CollectDatastoreClusters bool `yaml:"collect_datastore_clusters,omitempty" json:"collect_datastore_clusters"`
88 + DatastoreClustersInclude match.DatastoreClusterIncludes `yaml:"datastore_cluster_include,omitempty" json:"datastore_cluster_include"`
89 + CollectVSAN bool `yaml:"collect_vsan,omitempty" json:"collect_vsan"`
90 + VSANClustersInclude match.VSANClusterIncludes `yaml:"vsan_cluster_include,omitempty" json:"vsan_cluster_include"`
91 + VSANHostsInclude match.VSANHostIncludes `yaml:"vsan_host_include,omitempty" json:"vsan_host_include"`
92 + VSANVMsInclude match.VSANVMIncludes `yaml:"vsan_vm_include,omitempty" json:"vsan_vm_include"`
93 +
94 + // Opt-in label enrichment.
95 + TagCategories []string `yaml:"tag_categories,omitempty" json:"tag_categories"`
96 + CustomAttributes []string `yaml:"custom_attributes,omitempty" json:"custom_attributes"`
97 +
98 + // Optional cached topology Function data.
99 + CollectNetworkTopology bool `yaml:"collect_network_topology,omitempty" json:"collect_network_topology"`
100 }
101
102 type (
@@ -80,29 +104,28 @@ type (
104 collectorapi.Base
105 Config `yaml:",inline" json:""`
106
83 - charts *collectorapi.Charts
107 + store metrix.CollectorStore
108 + mx *collectorMetrics
109
110 + vsClient *clientpkg.Client
111 discoverer
112 scraper
113 dsPropertyCollector
114 clusterPropertyCollector
115 rpPropertyCollector
116
91 - collectionLock *sync.RWMutex
92 - resources *rs.Resources
93 - discoveryTask *task
94 - discoveredHosts map[string]int
95 - discoveredVMs map[string]int
96 - discoveredDatastores map[string]int
97 - discoveredClusters map[string]int
98 - discoveredResourcePools map[string]int
99 - charted map[string]bool
100 -
101 - // two-phase chart creation: property charts always, perf charts only when data arrives
102 - datastorePerfReceived map[string]bool
103 - datastorePerfCharted map[string]bool
104 - clusterPerfReceived map[string]bool
105 - clusterPerfCharted map[string]bool
117 + collectionLock *sync.RWMutex
118 + resources *rs.Resources
119 + discoveryTask *task
120 + datastoreClusterMatcher match.DatastoreClusterMatcher
121 + vsanClusterMatcher match.VSANClusterMatcher
122 + vsanHostMatcher match.VSANHostMatcher
123 + vsanVMMatcher match.VSANVMMatcher
124 + vsphereTagCategoryMatcher matcher.Matcher
125 + customAttributeMatcher matcher.Matcher
126 + hostPowerPerfSamples map[string]*hostPowerPerfSample
127 + vmPowerPerfSamples map[string]*vmPowerPerfSample
128 + vsanMetrics *scrapepkg.VSANMetrics
129 }
130 discoverer interface {
131 Discover() (*rs.Resources, error)
@@ -112,6 +135,7 @@ type (
135 ScrapeVMs(rs.VMs) []performance.EntityMetric
136 ScrapeDatastores(rs.Datastores) []performance.EntityMetric
137 ScrapeClusters(rs.Clusters) []performance.EntityMetric
138 + ScrapeVSAN(rs.Clusters, rs.Hosts, rs.VMs) *scrapepkg.VSANMetrics
139 }
140 dsPropertyCollector interface {
141 DatastoresByRef(refs []types.ManagedObjectReference, pathSet ...string) ([]mo25.Datastore, error)
@@ -129,23 +153,31 @@ func (c *Collector) Configuration() any {
153 }
154
155 func (c *Collector) Init(context.Context) error {
156 + c.ensureRuntimeState()
157 + c.stopDiscoveryTask(true)
158 + c.closeClient()
159 + c.resetRuntimeStateForInit()
160 +
161 if err := c.validateConfig(); err != nil {
133 - return fmt.Errorf("error on validating config: %v", err)
162 + return fmt.Errorf("validate vSphere collector configuration: %w", err)
163 }
164
165 vsClient, err := c.initClient()
166 if err != nil {
138 - return fmt.Errorf("error on creating vsphere client: %v", err)
167 + return fmt.Errorf("create vSphere client: %w", err)
168 }
169 + c.vsClient = vsClient
170
171 if err := c.initDiscoverer(vsClient); err != nil {
142 - return fmt.Errorf("error on creating vsphere discoverer: %v", err)
172 + c.closeClient()
173 + return fmt.Errorf("create vSphere discoverer from configuration: %w", err)
174 }
175
176 c.initScraper(vsClient)
177
178 if err := c.discoverOnce(); err != nil {
148 - return fmt.Errorf("error on discovering: %v", err)
179 + c.closeClient()
180 + return fmt.Errorf("run initial vSphere discovery: %w", err)
181 }
182
183 c.goDiscovery()
@@ -157,25 +189,75 @@ func (c *Collector) Check(context.Context) error {
189 return nil
190 }
191
160 -func (c *Collector) Charts() *collectorapi.Charts {
161 - return c.charts
192 +func (c *Collector) Collect(context.Context) error {
193 + c.collectionLock.Lock()
194 + defer c.collectionLock.Unlock()
195 +
196 + if err := c.collectLocked(); err != nil {
197 + return fmt.Errorf("collect vSphere metrics: %w", err)
198 + }
199 +
200 + return nil
201 }
202
164 -func (c *Collector) Collect(context.Context) map[string]int64 {
165 - mx, err := c.collect()
166 - if err != nil {
167 - c.Error(err)
203 +func (c *Collector) MetricStore() metrix.CollectorStore { return c.store }
204 +
205 +func (c *Collector) ChartTemplateYAML() string { return chartTemplateYAML }
206 +
207 +func (c *Collector) Cleanup(context.Context) {
208 + c.stopDiscoveryTask(true)
209 + c.closeClient()
210 +}
211 +
212 +func (c *Collector) ensureRuntimeState() {
213 + if c.collectionLock == nil {
214 + c.collectionLock = &sync.RWMutex{}
215 + }
216 + if c.store == nil {
217 + c.store = metrix.NewCollectorStore()
218 }
219 + if c.mx == nil {
220 + c.mx = newCollectorMetrics(c.store)
221 + }
222 +}
223 +
224 +func (c *Collector) resetRuntimeStateForInit() {
225 + c.collectionLock.Lock()
226 + defer c.collectionLock.Unlock()
227
170 - if len(mx) == 0 {
171 - return nil
228 + c.discoverer = nil
229 + c.scraper = nil
230 + c.dsPropertyCollector = nil
231 + c.clusterPropertyCollector = nil
232 + c.rpPropertyCollector = nil
233 + c.resources = nil
234 + c.datastoreClusterMatcher = nil
235 + c.vsanClusterMatcher = nil
236 + c.vsanHostMatcher = nil
237 + c.vsanVMMatcher = nil
238 + c.vsphereTagCategoryMatcher = nil
239 + c.customAttributeMatcher = nil
240 + c.hostPowerPerfSamples = nil
241 + c.vmPowerPerfSamples = nil
242 + c.vsanMetrics = nil
243 +}
244 +
245 +func (c *Collector) closeClient() {
246 + if c.vsClient == nil {
247 + return
248 }
173 - return mx
249 + if err := c.vsClient.Close(); err != nil {
250 + c.Warningf("close vSphere client during collector cleanup: %v", err)
251 + }
252 + c.vsClient = nil
253 }
254
176 -func (c *Collector) Cleanup(context.Context) {
255 +func (c *Collector) stopDiscoveryTask(wait bool) {
256 if c.discoveryTask == nil {
257 return
258 }
259 c.discoveryTask.stop()
260 + if wait {
261 + c.discoveryTask.wait()
262 + }
263 }
src/go/plugin/go.d/collector/vsphere/collector_test.go
+967 -670
@@ -2,8 +2,10 @@
2 package vsphere
3
4 import (
5 + "bytes"
6 "context"
7 "crypto/tls"
8 + "fmt"
9 "os"
10 "strings"
11 "testing"
@@ -13,9 +15,15 @@ import (
15 "github.com/stretchr/testify/require"
16 "github.com/vmware/govmomi/performance"
17 "github.com/vmware/govmomi/simulator"
18 + mo25 "github.com/vmware/govmomi/vim25/mo"
19 + "github.com/vmware/govmomi/vim25/types"
20
21 + "github.com/netdata/netdata/go/plugins/logger"
22 "github.com/netdata/netdata/go/plugins/pkg/confopt"
18 - "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/discover"
23 + "github.com/netdata/netdata/go/plugins/pkg/metrix"
24 + metrixselector "github.com/netdata/netdata/go/plugins/pkg/metrix/selector"
25 + "github.com/netdata/netdata/go/plugins/plugin/framework/chartengine"
26 + "github.com/netdata/netdata/go/plugins/plugin/framework/charttpl"
27 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/match"
28 rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
29 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
@@ -40,493 +48,941 @@ func TestCollector_ConfigurationSerialize(t *testing.T) {
48 }
49
50 func TestCollector_Init(t *testing.T) {
43 - collr, _, teardown := prepareVSphereSim(t)
44 - defer teardown()
51 + tests := map[string]struct {
52 + setup func(*Collector)
53 + wantErr string
54 + check func(*testing.T, *Collector)
55 + }{
56 + "success": {
57 + check: func(t *testing.T, collr *Collector) {
58 + assert.NotNil(t, collr.discoverer)
59 + assert.NotNil(t, collr.scraper)
60 + assert.NotNil(t, collr.resources)
61 + assert.NotNil(t, collr.discoveryTask)
62 + assert.True(t, collr.discoveryTask.isRunning())
63 + },
64 + },
65 + "URL not set": {
66 + setup: func(c *Collector) { c.URL = "" },
67 + wantErr: "url",
68 + },
69 + "username not set": {
70 + setup: func(c *Collector) { c.Username = "" },
71 + wantErr: "username",
72 + },
73 + "password not set": {
74 + setup: func(c *Collector) { c.Password = "" },
75 + wantErr: "password",
76 + },
77 + "discovery interval not positive": {
78 + setup: func(c *Collector) { c.DiscoveryInterval = 0 },
79 + wantErr: "discovery_interval must be greater than zero",
80 + },
81 + "wrong TLS CA": {
82 + setup: func(c *Collector) { c.ClientConfig.TLSConfig.TLSCA = "testdata/tls" },
83 + wantErr: "testdata/tls",
84 + },
85 + "connection refused": {
86 + setup: func(c *Collector) { c.URL = "http://127.0.0.1:32001" },
87 + wantErr: "connect",
88 + },
89 + "invalid host include format": {
90 + setup: func(c *Collector) { c.HostsInclude = match.HostIncludes{"invalid"} },
91 + wantErr: "host_include",
92 + },
93 + "invalid VM include format": {
94 + setup: func(c *Collector) { c.VMsInclude = match.VMIncludes{"invalid"} },
95 + wantErr: "vm_include",
96 + },
97 + "invalid datastore include format": {
98 + setup: func(c *Collector) { c.DatastoresInclude = match.DatastoreIncludes{"invalid"} },
99 + wantErr: "datastore_include",
100 + },
101 + "invalid cluster include format": {
102 + setup: func(c *Collector) { c.ClustersInclude = match.ClusterIncludes{"invalid"} },
103 + wantErr: "cluster_include",
104 + },
105 + }
106 +
107 + for name, tc := range tests {
108 + t.Run(name, func(t *testing.T) {
109 + collr, _, teardown := prepareVSphereSim(t)
110 + defer teardown()
111 + if tc.setup != nil {
112 + tc.setup(collr)
113 + }
114
46 - assert.NoError(t, collr.Init(context.Background()))
47 - assert.NotNil(t, collr.discoverer)
48 - assert.NotNil(t, collr.scraper)
49 - assert.NotNil(t, collr.resources)
50 - assert.NotNil(t, collr.discoveryTask)
51 - assert.True(t, collr.discoveryTask.isRunning())
115 + err := collr.Init(context.Background())
116 + if tc.wantErr != "" {
117 + require.ErrorContains(t, err, tc.wantErr)
118 + return
119 + }
120 + require.NoError(t, err)
121 + if tc.check != nil {
122 + tc.check(t, collr)
123 + }
124 + })
125 + }
126 }
127
54 -func TestCollector_Init_ReturnsFalseIfURLNotSet(t *testing.T) {
128 +func TestCollector_InitReentrantResetsRuntimeState(t *testing.T) {
129 collr, _, teardown := prepareVSphereSim(t)
130 defer teardown()
57 - collr.URL = ""
131
59 - assert.Error(t, collr.Init(context.Background()))
132 + require.NoError(t, collr.Init(context.Background()))
133 + collr.scraper = mockScraper{collr.scraper}
134 + firstRun := collectScalarSeriesForTest(t, collr)
135 + require.NotEmpty(t, firstRun)
136 +
137 + var keepHostName, keepHostID string
138 + for _, host := range collr.resources.Hosts {
139 + if keepHostID == "" {
140 + keepHostName = host.Name
141 + keepHostID = host.ID
142 + }
143 + }
144 + require.NotEmpty(t, keepHostName)
145 +
146 + collr.HostsInclude = match.HostIncludes{"/*/*/" + keepHostName}
147 + require.NoError(t, collr.Init(context.Background()))
148 + require.Len(t, collr.resources.Hosts, 1)
149 + require.NotNil(t, collr.resources.Hosts.Get(keepHostID))
150 +
151 + collr.scraper = mockScraper{collr.scraper}
152 + secondRun := collectScalarSeriesForTest(t, collr)
153 + require.NotEmpty(t, secondRun)
154 + require.True(t, scalarSeriesHasLabel(secondRun, "id", keepHostID))
155 +}
156 +
157 +func TestCollector_validateConfig_IgnoresDisabledOptionalSelectors(t *testing.T) {
158 + collr := New()
159 + collr.URL = "https://vcenter.local"
160 + collr.Username = "user"
161 + collr.Password = "[REDACTED_SECRET]"
162 + collr.DatastoreClustersInclude = match.DatastoreClusterIncludes{"!*"}
163 + collr.VSANClustersInclude = match.VSANClusterIncludes{"!*"}
164 + collr.VSANHostsInclude = match.VSANHostIncludes{"!*"}
165 + collr.VSANVMsInclude = match.VSANVMIncludes{"!*"}
166 +
167 + require.NoError(t, collr.validateConfig())
168 }
169
62 -func TestCollector_Init_ReturnsFalseIfUsernameNotSet(t *testing.T) {
63 - collr, _, teardown := prepareVSphereSim(t)
64 - defer teardown()
65 - collr.Username = ""
170 +func TestCollector_validateConfig_ValidatesEnabledOptionalSelectors(t *testing.T) {
171 + tests := map[string]struct {
172 + setup func(*Collector)
173 + want string
174 + }{
175 + "datastore clusters": {
176 + setup: func(c *Collector) {
177 + c.CollectDatastoreClusters = true
178 + c.DatastoreClustersInclude = match.DatastoreClusterIncludes{"!*"}
179 + },
180 + want: "datastore_cluster_include must include at least one positive pattern",
181 + },
182 + "vSAN clusters": {
183 + setup: func(c *Collector) {
184 + c.CollectVSAN = true
185 + c.VSANClustersInclude = match.VSANClusterIncludes{"!*"}
186 + },
187 + want: "vsan_cluster_include must include at least one positive pattern",
188 + },
189 + "vSAN hosts": {
190 + setup: func(c *Collector) {
191 + c.CollectVSAN = true
192 + c.VSANHostsInclude = match.VSANHostIncludes{"!*"}
193 + },
194 + want: "vsan_host_include must include at least one positive pattern",
195 + },
196 + "vSAN VMs": {
197 + setup: func(c *Collector) {
198 + c.CollectVSAN = true
199 + c.VSANVMsInclude = match.VSANVMIncludes{"!*"}
200 + },
201 + want: "vsan_vm_include must include at least one positive pattern",
202 + },
203 + }
204 +
205 + for name, tc := range tests {
206 + t.Run(name, func(t *testing.T) {
207 + collr := New()
208 + collr.URL = "https://vcenter.local"
209 + collr.Username = "user"
210 + collr.Password = "[REDACTED_SECRET]"
211 + tc.setup(collr)
212
67 - assert.Error(t, collr.Init(context.Background()))
213 + require.ErrorContains(t, collr.validateConfig(), tc.want)
214 + })
215 + }
216 }
217
70 -func TestCollector_Init_ReturnsFalseIfPasswordNotSet(t *testing.T) {
71 - collr, _, teardown := prepareVSphereSim(t)
72 - defer teardown()
73 - collr.Password = ""
218 +func TestCollector_Check(t *testing.T) {
219 + assert.NoError(t, New().Check(context.Background()))
220 +}
221 +
222 +func TestCollector_Cleanup(t *testing.T) {
223 + tests := map[string]struct {
224 + prepare func(t *testing.T) (*Collector, func())
225 + check func(t *testing.T, collr *Collector)
226 + }{
227 + "initialized": {
228 + prepare: func(t *testing.T) (*Collector, func()) {
229 + collr, _, teardown := prepareVSphereSim(t)
230 + require.NoError(t, collr.Init(context.Background()))
231 + return collr, teardown
232 + },
233 + check: func(t *testing.T, collr *Collector) {
234 + assert.True(t, collr.discoveryTask.isStopped())
235 + assert.False(t, collr.discoveryTask.isRunning())
236 + assert.Nil(t, collr.vsClient)
237 + },
238 + },
239 + "not initialized": {
240 + prepare: func(t *testing.T) (*Collector, func()) {
241 + return New(), func() {}
242 + },
243 + check: func(t *testing.T, collr *Collector) {
244 + assert.Nil(t, collr.vsClient)
245 + },
246 + },
247 + }
248 +
249 + for name, tc := range tests {
250 + t.Run(name, func(t *testing.T) {
251 + collr, cleanup := tc.prepare(t)
252 + defer cleanup()
253
75 - assert.Error(t, collr.Init(context.Background()))
254 + assert.NotPanics(t, func() { collr.Cleanup(context.Background()) })
255 + tc.check(t, collr)
256 + })
257 + }
258 }
259
78 -func TestCollector_Init_ReturnsFalseIfClientWrongTLSCA(t *testing.T) {
260 +func TestCollector_Collect(t *testing.T) {
261 collr, _, teardown := prepareVSphereSim(t)
262 defer teardown()
81 - collr.ClientConfig.TLSConfig.TLSCA = "testdata/tls"
263
83 - assert.Error(t, collr.Init(context.Background()))
264 + require.NoError(t, collr.Init(context.Background()))
265 + collr.scraper = mockScraper{collr.scraper}
266 +
267 + series := collectScalarSeriesForTest(t, collr)
268 + require.NotEmpty(t, series)
269 +
270 + requireScalarSeriesValue(t, series, "inventory_objects_datacenters", "inventory", 1)
271 + requireScalarSeriesValue(t, series, "inventory_objects_hosts", "inventory", 4)
272 + requireScalarSeriesValue(t, series, "inventory_objects_vms", "inventory", 4)
273 + requireScalarSeriesValue(t, series, "inventory_objects_datastores", "inventory", 1)
274 + requireScalarSeriesValue(t, series, "inventory_objects_resource_pools", "inventory", 1)
275 +
276 + requireScalarSeriesValue(t, series, "host_cpu_utilization_used", "host-21", 100)
277 + requireScalarSeriesValue(t, series, "host_disk_max_latency_latency", "host-21", 100)
278 + requireScalarSeriesValue(t, series, "host_net_errors_received", "host-21", 100)
279 + requireScalarSeriesValue(t, series, "host_overall_status_gray", "host-21", 1)
280 + requireScalarSeriesValue(t, series, "host_power_state_powered_on", "host-21", 1)
281 + requireScalarSeriesValue(t, series, "host_connection_state_connected", "host-21", 1)
282 + requireScalarSeriesValue(t, series, "host_maintenance_status_normal", "host-21", 1)
283 + requireScalarSeriesValue(t, series, "host_system_uptime_uptime", "host-21", 100)
284 +
285 + requireScalarSeriesValue(t, series, "vm_cpu_utilization_used", "vm-62", 200)
286 + requireScalarSeriesValue(t, series, "vm_disk_max_latency_latency", "vm-62", 200)
287 + requireScalarSeriesValue(t, series, "vm_net_drops_received", "vm-62", 200)
288 + requireScalarSeriesValue(t, series, "vm_overall_status_green", "vm-62", 1)
289 + requireScalarSeriesValue(t, series, "vm_power_state_powered_on", "vm-62", 1)
290 + requireScalarSeriesValue(t, series, "vm_tools_running_status_unknown", "vm-62", 1)
291 + requireScalarSeriesValue(t, series, "vm_tools_version_status_unknown", "vm-62", 1)
292 + requireScalarSeriesValue(t, series, "vm_config_cpu_vcpus", "vm-62", 1)
293 + requireScalarSeriesValue(t, series, "vm_config_devices_disks", "vm-62", 1)
294 + requireScalarSeriesValue(t, series, "vm_storage_usage_uncommitted", "vm-62", 10737418240)
295 + requireScalarSeriesValue(t, series, "vm_snapshot_count_count", "vm-62", 0)
296 +
297 + requireScalarSeriesValue(t, series, "datastore_space_usage_capacity", "datastore-59", 4398046511104)
298 + requireScalarSeriesValue(t, series, "datastore_space_usage_used", "datastore-59", 42949672960)
299 + requireScalarSeriesValue(t, series, "datastore_space_utilization_used", "datastore-59", 97)
300 + requireScalarSeriesValue(t, series, "datastore_overall_status_green", "datastore-59", 1)
301 + requireScalarSeriesValue(t, series, "datastore_accessibility_status_accessible", "datastore-59", 1)
302 + requireScalarSeriesValue(t, series, "datastore_maintenance_status_normal", "datastore-59", 1)
303 + requireScalarSeriesValue(t, series, "datastore_multiple_host_access_unknown", "datastore-59", 1)
304 + requireScalarSeriesValue(t, series, "datastore_disk_iops_reads", "datastore-59", 300)
305 +
306 + requireScalarSeriesValue(t, series, "cluster_hosts_total", "domain-c28", 3)
307 + requireScalarSeriesValue(t, series, "cluster_cpu_capacity_total", "domain-c28", 6882)
308 + requireScalarSeriesValue(t, series, "cluster_drs_config_enabled", "domain-c28", 1)
309 + requireScalarSeriesValue(t, series, "cluster_drs_mode_unknown", "domain-c28", 1)
310 + requireScalarSeriesValue(t, series, "cluster_ha_host_monitoring_unknown", "domain-c28", 1)
311 + requireScalarSeriesValue(t, series, "cluster_overall_status_green", "domain-c28", 1)
312 + requireScalarSeriesValue(t, series, "cluster_cpu_utilization_used", "domain-c28", 400)
313 + requireScalarSeriesValue(t, series, "cluster_services_fairness_cpu", "domain-c28", 400)
314 + requireScalarSeriesValue(t, series, "cluster_vm_migrations_vmotion", "domain-c28", 400)
315 +
316 + requireScalarSeriesValue(t, series, "resource_pool_cpu_usage_usage", "resgroup-27", 0)
317 + requireScalarSeriesValue(t, series, "resource_pool_cpu_allocation_max_usage", "resgroup-27", 4121)
318 + requireScalarSeriesValue(t, series, "resource_pool_mem_allocation_max_usage", "resgroup-27", 1007681536)
319 + requireScalarSeriesValue(t, series, "resource_pool_cpu_config_reservation", "resgroup-27", 4121)
320 + requireScalarSeriesValue(t, series, "resource_pool_mem_config_limit", "resgroup-27", 961)
321 + requireScalarSeriesValue(t, series, "resource_pool_overall_status_green", "resgroup-27", 1)
322 +
323 + collecttest.AssertChartCoverage(t, collr, collecttest.ChartCoverageExpectation{})
324 }
325
86 -func TestCollector_Init_ReturnsFalseIfConnectionRefused(t *testing.T) {
326 +func TestCollector_V2CompatibilitySurface(t *testing.T) {
327 collr, _, teardown := prepareVSphereSim(t)
328 defer teardown()
89 - collr.URL = "http://127.0.0.1:32001"
329
91 - assert.Error(t, collr.Init(context.Background()))
330 + require.NoError(t, collr.Init(context.Background()))
331 + collr.scraper = mockScraper{collr.scraper}
332 +
333 + require.NotEmpty(t, collectScalarSeriesForTest(t, collr))
334 +
335 + plan := buildV2PlanForTest(t, collr)
336 + createdCharts, createdDims := v2CreatedChartsAndDims(plan)
337 + require.NotEmpty(t, createdCharts)
338 +
339 + for chartID, chart := range createdCharts {
340 + require.NotEmpty(t, chart.Labels["id"], "chart %s must have the V2 instance id label", chartID)
341 + require.NotEmpty(t, createdDims[chartID], "chart %s must have dimensions", chartID)
342 + }
343 + collecttest.AssertChartCoverage(t, collr, collecttest.ChartCoverageExpectation{})
344 }
345
94 -func TestCollector_Init_ReturnsFalseIfInvalidHostVMIncludeFormat(t *testing.T) {
95 - collr, _, teardown := prepareVSphereSim(t)
96 - defer teardown()
346 +func TestSnapshotMaxAgeSeconds(t *testing.T) {
347 + now := time.Now()
348 + tests := map[string]struct {
349 + created time.Time
350 + want int64
351 + delta float64
352 + }{
353 + "zero time": {
354 + created: time.Time{},
355 + },
356 + "future time": {
357 + created: now.Add(time.Hour),
358 + },
359 + "past time": {
360 + created: now.Add(-2 * time.Hour),
361 + want: 7200,
362 + delta: 1,
363 + },
364 + }
365
98 - collr.HostsInclude = match.HostIncludes{"invalid"}
99 - assert.Error(t, collr.Init(context.Background()))
366 + for name, tc := range tests {
367 + t.Run(name, func(t *testing.T) {
368 + if tc.delta > 0 {
369 + assert.InDelta(t, tc.want, snapshotMaxAgeSeconds(tc.created), tc.delta)
370 + return
371 + }
372 + assert.EqualValues(t, tc.want, snapshotMaxAgeSeconds(tc.created))
373 + })
374 + }
375 +}
376
101 - collr.HostsInclude = collr.HostsInclude[:0]
377 +func TestCollectInventory(t *testing.T) {
378 + collr := New()
379 + collr.resources = &rs.Resources{
380 + DataCenters: rs.DataCenters{"dc-1": &rs.Datacenter{}},
381 + Folders: rs.Folders{"folder-1": &rs.Folder{}, "folder-2": &rs.Folder{}},
382 + Clusters: rs.Clusters{"domain-c1": &rs.Cluster{}},
383 + Hosts: rs.Hosts{"host-1": &rs.Host{}, "host-2": &rs.Host{}},
384 + VMs: rs.VMs{"vm-1": &rs.VM{}, "vm-2": &rs.VM{}, "vm-3": &rs.VM{}},
385 + Datastores: rs.Datastores{"datastore-1": &rs.Datastore{}},
386 + ResourcePools: rs.ResourcePools{"resgroup-1": &rs.ResourcePool{}},
387 + }
388
103 - collr.VMsInclude = match.VMIncludes{"invalid"}
104 - assert.Error(t, collr.Init(context.Background()))
389 + series := runMetricWriteForTest(t, collr, collr.collectInventory)
390
106 - collr.VMsInclude = collr.VMsInclude[:0]
391 + requireScalarSeriesValue(t, series, "inventory_objects_datacenters", "inventory", 1)
392 + requireScalarSeriesValue(t, series, "inventory_objects_folders", "inventory", 2)
393 + requireScalarSeriesValue(t, series, "inventory_objects_clusters", "inventory", 1)
394 + requireScalarSeriesValue(t, series, "inventory_objects_hosts", "inventory", 2)
395 + requireScalarSeriesValue(t, series, "inventory_objects_vms", "inventory", 3)
396 + requireScalarSeriesValue(t, series, "inventory_objects_datastores", "inventory", 1)
397 + requireScalarSeriesValue(t, series, "inventory_objects_resource_pools", "inventory", 1)
398 +}
399
108 - collr.DatastoresInclude = match.DatastoreIncludes{"invalid"}
109 - assert.Error(t, collr.Init(context.Background()))
400 +func TestCollector_Collect_NonPoweredResourcePropertyOnly(t *testing.T) {
401 + tests := map[string]struct {
402 + setup func(*Collector)
403 + collect func(*Collector) error
404 + want map[string]int64
405 + missing []string
406 + }{
407 + "host": {
408 + setup: func(c *Collector) {
409 + c.resources = &rs.Resources{
410 + Hosts: rs.Hosts{
411 + "host-1": &rs.Host{
412 + ID: "host-1",
413 + PowerState: string(types.HostSystemPowerStatePoweredOff),
414 + OverallStatus: "gray",
415 + },
416 + },
417 + }
418 + },
419 + collect: (*Collector).collectHosts,
420 + want: map[string]int64{
421 + "host_overall_status_gray": 1,
422 + "host_power_state_powered_off": 1,
423 + "host_power_state_powered_on": 0,
424 + "host_connection_state_connected": 0,
425 + },
426 + missing: []string{"host_cpu_utilization_used"},
427 + },
428 + "VM": {
429 + setup: func(c *Collector) {
430 + c.resources = &rs.Resources{
431 + VMs: rs.VMs{
432 + "vm-1": &rs.VM{
433 + ID: "vm-1",
434 + PowerState: string(types.VirtualMachinePowerStateSuspended),
435 + OverallStatus: "yellow",
436 + SnapshotCount: 2,
437 + SnapshotMaxChainDepth: 1,
438 + },
439 + },
440 + }
441 + },
442 + collect: (*Collector).collectVMs,
443 + want: map[string]int64{
444 + "vm_overall_status_yellow": 1,
445 + "vm_power_state_suspended": 1,
446 + "vm_power_state_powered_on": 0,
447 + "vm_snapshot_count_count": 2,
448 + "vm_snapshot_max_chain_depth_depth": 1,
449 + },
450 + missing: []string{"vm_cpu_utilization_used"},
451 + },
452 + }
453
111 - collr.DatastoresInclude = collr.DatastoresInclude[:0]
454 + for name, tc := range tests {
455 + t.Run(name, func(t *testing.T) {
456 + collr := New()
457 + tc.setup(collr)
458
113 - collr.ClustersInclude = match.ClusterIncludes{"invalid"}
114 - assert.Error(t, collr.Init(context.Background()))
459 + series := runMetricCollectForTest(t, collr, func() error { return tc.collect(collr) })
460 + for metric, want := range tc.want {
461 + requireScalarSeriesValue(t, series, metric, firstResourceID(collr), want)
462 + }
463 + for _, metric := range tc.missing {
464 + requireNoScalarSeries(t, series, metric, firstResourceID(collr))
465 + }
466 + })
467 + }
468 }
469
117 -func TestCollector_Check(t *testing.T) {
118 - assert.NoError(t, New().Check(context.Background()))
470 +func TestCollector_Collect_NoPerfDataKeepsPropertyMetrics(t *testing.T) {
471 + tests := map[string]struct {
472 + setup func(*Collector)
473 + check func(*testing.T, *Collector, map[string]metrix.SampleValue)
474 + }{
475 + "hosts": {
476 + setup: func(c *Collector) { c.scraper = mockScraperNoHostPerf{mockScraper{c.scraper}} },
477 + check: func(t *testing.T, collr *Collector, series map[string]metrix.SampleValue) {
478 + for _, host := range collr.resources.Hosts {
479 + requireScalarSeries(t, series, "host_overall_status_green", host.ID)
480 + requireScalarSeries(t, series, "host_power_state_powered_on", host.ID)
481 + requireNoScalarSeries(t, series, "host_cpu_utilization_used", host.ID)
482 + }
483 + for _, vm := range collr.resources.VMs {
484 + requireScalarSeries(t, series, "vm_cpu_utilization_used", vm.ID)
485 + }
486 + },
487 + },
488 + "VMs": {
489 + setup: func(c *Collector) { c.scraper = mockScraperNoVMPerf{mockScraper{c.scraper}} },
490 + check: func(t *testing.T, collr *Collector, series map[string]metrix.SampleValue) {
491 + for _, host := range collr.resources.Hosts {
492 + requireScalarSeries(t, series, "host_cpu_utilization_used", host.ID)
493 + }
494 + for _, vm := range collr.resources.VMs {
495 + requireScalarSeries(t, series, "vm_overall_status_green", vm.ID)
496 + requireScalarSeries(t, series, "vm_power_state_powered_on", vm.ID)
497 + requireNoScalarSeries(t, series, "vm_cpu_utilization_used", vm.ID)
498 + }
499 + },
500 + },
501 + }
502 +
503 + for name, tc := range tests {
504 + t.Run(name, func(t *testing.T) {
505 + collr, _, teardown := prepareVSphereSim(t)
506 + defer teardown()
507 +
508 + require.NoError(t, collr.Init(context.Background()))
509 + tc.setup(collr)
510 +
511 + series := collectScalarSeriesForTest(t, collr)
512 + require.NotNil(t, series)
513 + tc.check(t, collr, series)
514 + })
515 + }
516 +}
517 +
518 +func TestCollector_Collect_PropertyRefreshFailureSkipsStalePropertyMetrics(t *testing.T) {
519 + tests := map[string]struct {
520 + setup func(*Collector)
521 + check func(*testing.T, *Collector, map[string]metrix.SampleValue)
522 + }{
523 + "datastores": {
524 + setup: func(c *Collector) { c.dsPropertyCollector = nil },
525 + check: func(t *testing.T, collr *Collector, series map[string]metrix.SampleValue) {
526 + for _, ds := range collr.resources.Datastores {
527 + requireNoScalarSeries(t, series, "datastore_space_usage_capacity", ds.ID)
528 + requireNoScalarSeries(t, series, "datastore_overall_status_green", ds.ID)
529 + requireScalarSeries(t, series, "datastore_disk_io_read", ds.ID)
530 + }
531 + },
532 + },
533 + "clusters": {
534 + setup: func(c *Collector) { c.clusterPropertyCollector = nil },
535 + check: func(t *testing.T, collr *Collector, series map[string]metrix.SampleValue) {
536 + for _, cl := range collr.resources.Clusters {
537 + requireNoScalarSeries(t, series, "cluster_hosts_total", cl.ID)
538 + requireNoScalarSeries(t, series, "cluster_overall_status_green", cl.ID)
539 + requireScalarSeries(t, series, "cluster_cpu_utilization_used", cl.ID)
540 + }
541 + },
542 + },
543 + "resource pools": {
544 + setup: func(c *Collector) { c.rpPropertyCollector = nil },
545 + check: func(t *testing.T, collr *Collector, series map[string]metrix.SampleValue) {
546 + for _, rp := range collr.resources.ResourcePools {
547 + requireNoScalarSeries(t, series, "resource_pool_cpu_usage_usage", rp.ID)
548 + requireNoScalarSeries(t, series, "resource_pool_overall_status_green", rp.ID)
549 + }
550 + },
551 + },
552 + }
553 +
554 + for name, tc := range tests {
555 + t.Run(name, func(t *testing.T) {
556 + collr, _, teardown := prepareVSphereSim(t)
557 + defer teardown()
558 +
559 + require.NoError(t, collr.Init(context.Background()))
560 + collr.scraper = mockScraper{collr.scraper}
561 + tc.setup(collr)
562 +
563 + series := collectScalarSeriesForTest(t, collr)
564 + require.NotNil(t, series)
565 + tc.check(t, collr, series)
566 + })
567 + }
568 }
569
121 -func TestCollector_Charts(t *testing.T) {
122 - assert.NotNil(t, New().Charts())
570 +func TestCollector_Collect_NoPerfDataWarningIsRateLimited(t *testing.T) {
571 + tests := map[string]struct {
572 + setup func(*Collector)
573 + collect func(*Collector) error
574 + wantLog string
575 + }{
576 + "hosts": {
577 + setup: func(c *Collector) {
578 + c.scraper = mockScraperNoHostPerf{}
579 + c.resources = &rs.Resources{
580 + Hosts: rs.Hosts{
581 + "host-1": &rs.Host{
582 + ID: "host-1",
583 + PowerState: string(types.HostSystemPowerStatePoweredOn),
584 + },
585 + },
586 + }
587 + },
588 + collect: (*Collector).collectHosts,
589 + wantLog: "collect host performance metrics",
590 + },
591 + "VMs": {
592 + setup: func(c *Collector) {
593 + c.scraper = mockScraperNoVMPerf{}
594 + c.resources = &rs.Resources{
595 + VMs: rs.VMs{
596 + "vm-1": &rs.VM{
597 + ID: "vm-1",
598 + PowerState: string(types.VirtualMachinePowerStatePoweredOn),
599 + },
600 + },
601 + }
602 + },
603 + collect: (*Collector).collectVMs,
604 + wantLog: "collect VM performance metrics",
605 + },
606 + }
607 +
608 + for name, tc := range tests {
609 + t.Run(name, func(t *testing.T) {
610 + var buf bytes.Buffer
611 + collr := New()
612 + collr.Logger = logger.NewWithWriter(&buf)
613 + tc.setup(collr)
614 +
615 + runMetricCollectForTest(t, collr, func() error { return tc.collect(collr) })
616 + runMetricCollectForTest(t, collr, func() error { return tc.collect(collr) })
617 +
618 + assert.Equal(t, 1, strings.Count(buf.String(), tc.wantLog))
619 + })
620 + }
621 }
622
125 -func TestCollector_Cleanup(t *testing.T) {
126 - collr, _, teardown := prepareVSphereSim(t)
127 - defer teardown()
623 +func TestWriteHostPropertyMetrics_RuntimeStatus(t *testing.T) {
624 + host := &rs.Host{
625 + ID: "host-1",
626 + ConnectionState: string(types.HostSystemConnectionStateNotResponding),
627 + PowerState: string(types.HostSystemPowerStatePoweredOn),
628 + InMaintenanceMode: true,
629 + OverallStatus: "yellow",
630 + }
631 + collr := New()
632
129 - require.NoError(t, collr.Init(context.Background()))
633 + series := runMetricWriteForTest(t, collr, func() { collr.writeHostPropertyMetrics(host) })
634
131 - collr.Cleanup(context.Background())
132 - time.Sleep(time.Second)
133 - assert.True(t, collr.discoveryTask.isStopped())
134 - assert.False(t, collr.discoveryTask.isRunning())
635 + requireScalarSeriesValue(t, series, "host_connection_state_not_responding", host.ID, 1)
636 + requireScalarSeriesValue(t, series, "host_connection_state_connected", host.ID, 0)
637 + requireScalarSeriesValue(t, series, "host_maintenance_status_in_maintenance", host.ID, 1)
638 + requireScalarSeriesValue(t, series, "host_maintenance_status_normal", host.ID, 0)
639 }
640
137 -func TestCollector_Cleanup_NotPanicsIfNotInitialized(t *testing.T) {
138 - assert.NotPanics(t, func() { New().Cleanup(context.Background()) })
641 +func TestWriteVMPropertyMetrics_StatusConfigAndStorage(t *testing.T) {
642 + vm := &rs.VM{
643 + ID: "vm-1",
644 + ConnectionState: string(types.VirtualMachineConnectionStateInaccessible),
645 + PowerState: string(types.VirtualMachinePowerStatePoweredOn),
646 + ToolsRunningStatus: string(types.VirtualMachineToolsRunningStatusGuestToolsRunning),
647 + ToolsVersionStatus: string(types.VirtualMachineToolsVersionStatusGuestToolsTooOld),
648 + ConsolidationNeeded: true,
649 + ConfigCPU: 4,
650 + ConfigMemory: 8192,
651 + ConfigDisks: 2,
652 + ConfigNICs: 3,
653 + StorageCommitted: 100,
654 + StorageUncommitted: 200,
655 + StorageUnshared: 300,
656 + OverallStatus: "green",
657 + }
658 + collr := New()
659 +
660 + series := runMetricWriteForTest(t, collr, func() { collr.writeVMPropertyMetrics(vm) })
661 +
662 + requireScalarSeriesValue(t, series, "vm_connection_state_inaccessible", vm.ID, 1)
663 + requireScalarSeriesValue(t, series, "vm_connection_state_connected", vm.ID, 0)
664 + requireScalarSeriesValue(t, series, "vm_tools_running_status_running", vm.ID, 1)
665 + requireScalarSeriesValue(t, series, "vm_tools_running_status_unknown", vm.ID, 0)
666 + requireScalarSeriesValue(t, series, "vm_tools_version_status_too_old", vm.ID, 1)
667 + requireScalarSeriesValue(t, series, "vm_tools_version_status_unknown", vm.ID, 0)
668 + requireScalarSeriesValue(t, series, "vm_consolidation_needed_needed", vm.ID, 1)
669 + requireScalarSeriesValue(t, series, "vm_consolidation_needed_not_needed", vm.ID, 0)
670 + requireScalarSeriesValue(t, series, "vm_config_cpu_vcpus", vm.ID, 4)
671 + requireScalarSeriesValue(t, series, "vm_config_memory_memory", vm.ID, 8192)
672 + requireScalarSeriesValue(t, series, "vm_config_devices_disks", vm.ID, 2)
673 + requireScalarSeriesValue(t, series, "vm_config_devices_nics", vm.ID, 3)
674 + requireScalarSeriesValue(t, series, "vm_storage_usage_committed", vm.ID, 100)
675 + requireScalarSeriesValue(t, series, "vm_storage_usage_uncommitted", vm.ID, 200)
676 + requireScalarSeriesValue(t, series, "vm_storage_usage_unshared", vm.ID, 300)
677 }
678
141 -func TestCollector_Collect(t *testing.T) {
142 - collr, model, teardown := prepareVSphereSim(t)
143 - defer teardown()
679 +func TestWriteClusterPropertyMetrics_ConfigStates(t *testing.T) {
680 + cluster := &rs.Cluster{
681 + ID: "domain-c1",
682 + DrsEnabled: true,
683 + DrsMode: string(types.DrsBehaviorFullyAutomated),
684 + DrsVmotionRate: 3,
685 + HaEnabled: true,
686 + HaAdmCtrlEnabled: true,
687 + HaHostMonitoring: string(types.ClusterDasConfigInfoServiceStateEnabled),
688 + HaVMMonitoring: string(types.ClusterDasConfigInfoVmMonitoringStateVmAndAppMonitoring),
689 + HaVMComponentProtection: string(types.ClusterDasConfigInfoServiceStateDisabled),
690 + OverallStatus: "green",
691 + }
692 + collr := New()
693 +
694 + series := runMetricWriteForTest(t, collr, func() { collr.writeClusterPropertyMetrics(cluster) })
695 +
696 + requireScalarSeriesValue(t, series, "cluster_drs_config_enabled", cluster.ID, 1)
697 + requireScalarSeriesValue(t, series, "cluster_drs_mode_fully_automated", cluster.ID, 1)
698 + requireScalarSeriesValue(t, series, "cluster_drs_mode_unknown", cluster.ID, 0)
699 + requireScalarSeriesValue(t, series, "cluster_drs_vmotion_rate_rate", cluster.ID, 3)
700 + requireScalarSeriesValue(t, series, "cluster_ha_config_enabled", cluster.ID, 1)
701 + requireScalarSeriesValue(t, series, "cluster_ha_config_admission_control", cluster.ID, 1)
702 + requireScalarSeriesValue(t, series, "cluster_ha_host_monitoring_enabled", cluster.ID, 1)
703 + requireScalarSeriesValue(t, series, "cluster_ha_host_monitoring_unknown", cluster.ID, 0)
704 + requireScalarSeriesValue(t, series, "cluster_ha_vm_monitoring_vm_and_app_monitoring", cluster.ID, 1)
705 + requireScalarSeriesValue(t, series, "cluster_ha_vm_monitoring_unknown", cluster.ID, 0)
706 + requireScalarSeriesValue(t, series, "cluster_ha_vm_component_protection_disabled", cluster.ID, 1)
707 + requireScalarSeriesValue(t, series, "cluster_ha_vm_component_protection_unknown", cluster.ID, 0)
708 +}
709
145 - require.NoError(t, collr.Init(context.Background()))
710 +func TestUpdateResourcePoolFromProperties_ZeroValueOptionalProperties(t *testing.T) {
711 + rp := &rs.ResourcePool{
712 + ID: "resgroup-1",
713 + OverallCpuUsage: 1,
714 + OverallCpuDemand: 2,
715 + GuestMemoryUsage: 3,
716 + HostMemoryUsage: 4,
717 + DistributedCpuEntitlement: 5,
718 + DistributedMemoryEntitlement: 6,
719 + PrivateMemory: 7,
720 + SharedMemory: 8,
721 + SwappedMemory: 9,
722 + BalloonedMemory: 10,
723 + OverheadMemory: 11,
724 + ConsumedOverheadMemory: 12,
725 + CompressedMemory: 13,
726 + CpuReservationUsed: 14,
727 + CpuMaxUsage: 15,
728 + CpuUnreservedForVm: 16,
729 + MemReservationUsed: 17,
730 + MemMaxUsage: 18,
731 + MemUnreservedForVm: 19,
732 + CpuReservation: 20,
733 + CpuLimit: 21,
734 + MemReservation: 22,
735 + MemLimit: 23,
736 + }
737
147 - collr.scraper = mockScraper{collr.scraper}
738 + require.NotPanics(t, func() {
739 + updateResourcePoolFromProperties(rp, mo25.ResourcePool{})
740 + })
741 +
742 + assert.Zero(t, rp.OverallCpuUsage)
743 + assert.Zero(t, rp.GuestMemoryUsage)
744 + assert.Zero(t, rp.CpuReservationUsed)
745 + assert.Zero(t, rp.MemReservationUsed)
746 + assert.Zero(t, rp.CpuReservation)
747 + assert.EqualValues(t, -1, rp.CpuLimit)
748 + assert.Zero(t, rp.MemReservation)
749 + assert.EqualValues(t, -1, rp.MemLimit)
750 +}
751
149 - expected := map[string]int64{
150 - "host-21_cpu.usage.average": 100,
151 - "host-21_disk.maxTotalLatency.latest": 100,
152 - "host-21_disk.read.average": 100,
153 - "host-21_disk.write.average": 100,
154 - "host-21_mem.active.average": 100,
155 - "host-21_mem.consumed.average": 100,
156 - "host-21_mem.granted.average": 100,
157 - "host-21_mem.shared.average": 100,
158 - "host-21_mem.sharedcommon.average": 100,
159 - "host-21_mem.swapinRate.average": 100,
160 - "host-21_mem.swapoutRate.average": 100,
161 - "host-21_mem.usage.average": 100,
162 - "host-21_net.bytesRx.average": 100,
163 - "host-21_net.bytesTx.average": 100,
164 - "host-21_net.droppedRx.summation": 100,
165 - "host-21_net.droppedTx.summation": 100,
166 - "host-21_net.errorsRx.summation": 100,
167 - "host-21_net.errorsTx.summation": 100,
168 - "host-21_net.packetsRx.summation": 100,
169 - "host-21_net.packetsTx.summation": 100,
170 - "host-21_overall.status.gray": 1,
171 - "host-21_overall.status.green": 0,
172 - "host-21_overall.status.red": 0,
173 - "host-21_overall.status.yellow": 0,
174 - "host-21_sys.uptime.latest": 100,
175 - "host-37_cpu.usage.average": 100,
176 - "host-37_disk.maxTotalLatency.latest": 100,
177 - "host-37_disk.read.average": 100,
178 - "host-37_disk.write.average": 100,
179 - "host-37_mem.active.average": 100,
180 - "host-37_mem.consumed.average": 100,
181 - "host-37_mem.granted.average": 100,
182 - "host-37_mem.shared.average": 100,
183 - "host-37_mem.sharedcommon.average": 100,
184 - "host-37_mem.swapinRate.average": 100,
185 - "host-37_mem.swapoutRate.average": 100,
186 - "host-37_mem.usage.average": 100,
187 - "host-37_net.bytesRx.average": 100,
188 - "host-37_net.bytesTx.average": 100,
189 - "host-37_net.droppedRx.summation": 100,
190 - "host-37_net.droppedTx.summation": 100,
191 - "host-37_net.errorsRx.summation": 100,
192 - "host-37_net.errorsTx.summation": 100,
193 - "host-37_net.packetsRx.summation": 100,
194 - "host-37_net.packetsTx.summation": 100,
195 - "host-37_overall.status.gray": 1,
196 - "host-37_overall.status.green": 0,
197 - "host-37_overall.status.red": 0,
198 - "host-37_overall.status.yellow": 0,
199 - "host-37_sys.uptime.latest": 100,
200 - "host-47_cpu.usage.average": 100,
201 - "host-47_disk.maxTotalLatency.latest": 100,
202 - "host-47_disk.read.average": 100,
203 - "host-47_disk.write.average": 100,
204 - "host-47_mem.active.average": 100,
205 - "host-47_mem.consumed.average": 100,
206 - "host-47_mem.granted.average": 100,
207 - "host-47_mem.shared.average": 100,
208 - "host-47_mem.sharedcommon.average": 100,
209 - "host-47_mem.swapinRate.average": 100,
210 - "host-47_mem.swapoutRate.average": 100,
211 - "host-47_mem.usage.average": 100,
212 - "host-47_net.bytesRx.average": 100,
213 - "host-47_net.bytesTx.average": 100,
214 - "host-47_net.droppedRx.summation": 100,
215 - "host-47_net.droppedTx.summation": 100,
216 - "host-47_net.errorsRx.summation": 100,
217 - "host-47_net.errorsTx.summation": 100,
218 - "host-47_net.packetsRx.summation": 100,
219 - "host-47_net.packetsTx.summation": 100,
220 - "host-47_overall.status.gray": 1,
221 - "host-47_overall.status.green": 0,
222 - "host-47_overall.status.red": 0,
223 - "host-47_overall.status.yellow": 0,
224 - "host-47_sys.uptime.latest": 100,
225 - "host-57_cpu.usage.average": 100,
226 - "host-57_disk.maxTotalLatency.latest": 100,
227 - "host-57_disk.read.average": 100,
228 - "host-57_disk.write.average": 100,
229 - "host-57_mem.active.average": 100,
230 - "host-57_mem.consumed.average": 100,
231 - "host-57_mem.granted.average": 100,
232 - "host-57_mem.shared.average": 100,
233 - "host-57_mem.sharedcommon.average": 100,
234 - "host-57_mem.swapinRate.average": 100,
235 - "host-57_mem.swapoutRate.average": 100,
236 - "host-57_mem.usage.average": 100,
237 - "host-57_net.bytesRx.average": 100,
238 - "host-57_net.bytesTx.average": 100,
239 - "host-57_net.droppedRx.summation": 100,
240 - "host-57_net.droppedTx.summation": 100,
241 - "host-57_net.errorsRx.summation": 100,
242 - "host-57_net.errorsTx.summation": 100,
243 - "host-57_net.packetsRx.summation": 100,
244 - "host-57_net.packetsTx.summation": 100,
245 - "host-57_overall.status.gray": 1,
246 - "host-57_overall.status.green": 0,
247 - "host-57_overall.status.red": 0,
248 - "host-57_overall.status.yellow": 0,
249 - "host-57_sys.uptime.latest": 100,
250 - "vm-62_cpu.usage.average": 200,
251 - "vm-62_disk.maxTotalLatency.latest": 200,
252 - "vm-62_disk.read.average": 200,
253 - "vm-62_disk.write.average": 200,
254 - "vm-62_mem.active.average": 200,
255 - "vm-62_mem.consumed.average": 200,
256 - "vm-62_mem.granted.average": 200,
257 - "vm-62_mem.shared.average": 200,
258 - "vm-62_mem.swapinRate.average": 200,
259 - "vm-62_mem.swapoutRate.average": 200,
260 - "vm-62_mem.swapped.average": 200,
261 - "vm-62_mem.usage.average": 200,
262 - "vm-62_net.bytesRx.average": 200,
263 - "vm-62_net.bytesTx.average": 200,
264 - "vm-62_net.droppedRx.summation": 200,
265 - "vm-62_net.droppedTx.summation": 200,
266 - "vm-62_net.packetsRx.summation": 200,
267 - "vm-62_net.packetsTx.summation": 200,
268 - "vm-62_overall.status.gray": 0,
269 - "vm-62_overall.status.green": 1,
270 - "vm-62_overall.status.red": 0,
271 - "vm-62_overall.status.yellow": 0,
272 - "vm-62_sys.uptime.latest": 200,
273 - "vm-65_cpu.usage.average": 200,
274 - "vm-65_disk.maxTotalLatency.latest": 200,
275 - "vm-65_disk.read.average": 200,
276 - "vm-65_disk.write.average": 200,
277 - "vm-65_mem.active.average": 200,
278 - "vm-65_mem.consumed.average": 200,
279 - "vm-65_mem.granted.average": 200,
280 - "vm-65_mem.shared.average": 200,
281 - "vm-65_mem.swapinRate.average": 200,
282 - "vm-65_mem.swapoutRate.average": 200,
283 - "vm-65_mem.swapped.average": 200,
284 - "vm-65_mem.usage.average": 200,
285 - "vm-65_net.bytesRx.average": 200,
286 - "vm-65_net.bytesTx.average": 200,
287 - "vm-65_net.droppedRx.summation": 200,
288 - "vm-65_net.droppedTx.summation": 200,
289 - "vm-65_net.packetsRx.summation": 200,
290 - "vm-65_net.packetsTx.summation": 200,
291 - "vm-65_overall.status.gray": 0,
292 - "vm-65_overall.status.green": 1,
293 - "vm-65_overall.status.red": 0,
294 - "vm-65_overall.status.yellow": 0,
295 - "vm-65_sys.uptime.latest": 200,
296 - "vm-68_cpu.usage.average": 200,
297 - "vm-68_disk.maxTotalLatency.latest": 200,
298 - "vm-68_disk.read.average": 200,
299 - "vm-68_disk.write.average": 200,
300 - "vm-68_mem.active.average": 200,
301 - "vm-68_mem.consumed.average": 200,
302 - "vm-68_mem.granted.average": 200,
303 - "vm-68_mem.shared.average": 200,
304 - "vm-68_mem.swapinRate.average": 200,
305 - "vm-68_mem.swapoutRate.average": 200,
306 - "vm-68_mem.swapped.average": 200,
307 - "vm-68_mem.usage.average": 200,
308 - "vm-68_net.bytesRx.average": 200,
309 - "vm-68_net.bytesTx.average": 200,
310 - "vm-68_net.droppedRx.summation": 200,
311 - "vm-68_net.droppedTx.summation": 200,
312 - "vm-68_net.packetsRx.summation": 200,
313 - "vm-68_net.packetsTx.summation": 200,
314 - "vm-68_overall.status.gray": 0,
315 - "vm-68_overall.status.green": 1,
316 - "vm-68_overall.status.red": 0,
317 - "vm-68_overall.status.yellow": 0,
318 - "vm-68_sys.uptime.latest": 200,
319 - "vm-71_cpu.usage.average": 200,
320 - "vm-71_disk.maxTotalLatency.latest": 200,
321 - "vm-71_disk.read.average": 200,
322 - "vm-71_disk.write.average": 200,
323 - "vm-71_mem.active.average": 200,
324 - "vm-71_mem.consumed.average": 200,
325 - "vm-71_mem.granted.average": 200,
326 - "vm-71_mem.shared.average": 200,
327 - "vm-71_mem.swapinRate.average": 200,
328 - "vm-71_mem.swapoutRate.average": 200,
329 - "vm-71_mem.swapped.average": 200,
330 - "vm-71_mem.usage.average": 200,
331 - "vm-71_net.bytesRx.average": 200,
332 - "vm-71_net.bytesTx.average": 200,
333 - "vm-71_net.droppedRx.summation": 200,
334 - "vm-71_net.droppedTx.summation": 200,
335 - "vm-71_net.packetsRx.summation": 200,
336 - "vm-71_net.packetsTx.summation": 200,
337 - "vm-71_overall.status.gray": 0,
338 - "vm-71_overall.status.green": 1,
339 - "vm-71_overall.status.red": 0,
340 - "vm-71_overall.status.yellow": 0,
341 - "vm-71_sys.uptime.latest": 200,
342 - "datastore-59_capacity": 4398046511104,
343 - "datastore-59_free_space": 4355096838144,
344 - "datastore-59_used_space": 42949672960,
345 - "datastore-59_used_space_pct": 97,
346 - "datastore-59_overall.status.green": 1,
347 - "datastore-59_overall.status.gray": 0,
348 - "datastore-59_overall.status.red": 0,
349 - "datastore-59_overall.status.yellow": 0,
350 - "datastore-59_datastore.numberReadAveraged.average": 300,
351 - "datastore-59_datastore.numberWriteAveraged.average": 300,
352 - "datastore-59_datastore.read.average": 300,
353 - "datastore-59_datastore.write.average": 300,
354 - "datastore-59_datastore.totalReadLatency.average": 300,
355 - "datastore-59_datastore.totalWriteLatency.average": 300,
356 - // Cluster property metrics (domain-c28)
357 - "domain-c28_num_hosts": 3,
358 - "domain-c28_num_effective_hosts": 3,
359 - "domain-c28_total_cpu": 6882,
360 - "domain-c28_effective_cpu": 6882,
361 - "domain-c28_total_memory": 12883292160,
362 - "domain-c28_effective_memory": 13509110959964160,
363 - "domain-c28_num_cpu_cores": 6,
364 - "domain-c28_num_cpu_threads": 6,
365 - "domain-c28_num_vmotions": 0,
366 - "domain-c28_drs_score": 0,
367 - "domain-c28_current_balance": 0,
368 - "domain-c28_target_balance": 0,
369 - "domain-c28_drs_enabled": 1,
370 - "domain-c28_ha_enabled": 0,
371 - "domain-c28_ha_adm_ctrl_enabled": 0,
372 - "domain-c28_usage_cpu_demand_mhz": 0,
373 - "domain-c28_usage_mem_demand_mb": 0,
374 - "domain-c28_usage_cpu_entitled_mhz": 0,
375 - "domain-c28_usage_mem_entitled_mb": 0,
376 - "domain-c28_usage_cpu_reservation_mhz": 0,
377 - "domain-c28_usage_mem_reservation_mb": 0,
378 - "domain-c28_usage_total_vm_count": 0,
379 - "domain-c28_usage_powered_off_vm_count": 0,
380 - "domain-c28_overall.status.green": 1,
381 - "domain-c28_overall.status.gray": 0,
382 - "domain-c28_overall.status.red": 0,
383 - "domain-c28_overall.status.yellow": 0,
384 - // Cluster perf metrics (domain-c28)
385 - "domain-c28_clusterServices.cpufairness.latest": 400,
386 - "domain-c28_clusterServices.effectivecpu.average": 400,
387 - "domain-c28_clusterServices.effectivemem.average": 400,
388 - "domain-c28_clusterServices.failover.latest": 400,
389 - "domain-c28_clusterServices.memfairness.latest": 400,
390 - "domain-c28_cpu.totalmhz.average": 400,
391 - "domain-c28_cpu.usage.average": 400,
392 - "domain-c28_cpu.usagemhz.average": 400,
393 - "domain-c28_mem.active.average": 400,
394 - "domain-c28_mem.consumed.average": 400,
395 - "domain-c28_mem.granted.average": 400,
396 - "domain-c28_mem.overhead.average": 400,
397 - "domain-c28_mem.shared.average": 400,
398 - "domain-c28_mem.swapused.average": 400,
399 - "domain-c28_mem.usage.average": 400,
400 - "domain-c28_vmop.numChangeDS.latest": 400,
401 - "domain-c28_vmop.numChangeHost.latest": 400,
402 - "domain-c28_vmop.numChangeHostDS.latest": 400,
403 - "domain-c28_vmop.numClone.latest": 400,
404 - "domain-c28_vmop.numCreate.latest": 400,
405 - "domain-c28_vmop.numDeploy.latest": 400,
406 - "domain-c28_vmop.numDestroy.latest": 400,
407 - "domain-c28_vmop.numPoweroff.latest": 400,
408 - "domain-c28_vmop.numPoweron.latest": 400,
409 - "domain-c28_vmop.numRebootGuest.latest": 400,
410 - "domain-c28_vmop.numReconfigure.latest": 400,
411 - "domain-c28_vmop.numRegister.latest": 400,
412 - "domain-c28_vmop.numReset.latest": 400,
413 - "domain-c28_vmop.numSVMotion.latest": 400,
414 - "domain-c28_vmop.numShutdownGuest.latest": 400,
415 - "domain-c28_vmop.numStandbyGuest.latest": 400,
416 - "domain-c28_vmop.numSuspend.latest": 400,
417 - "domain-c28_vmop.numUnregister.latest": 400,
418 - "domain-c28_vmop.numVMotion.latest": 400,
419 - "domain-c28_vmop.numXVMotion.latest": 400,
420 - // Resource pool metrics (resgroup-27)
421 - "resgroup-27_cpu_usage": 0,
422 - "resgroup-27_cpu_demand": 0,
423 - "resgroup-27_cpu_entitlement_distributed": 0,
424 - "resgroup-27_mem_usage_guest": 0,
425 - "resgroup-27_mem_usage_host": 0,
426 - "resgroup-27_mem_entitlement_distributed": 0,
427 - "resgroup-27_mem_private": 0,
428 - "resgroup-27_mem_shared": 0,
429 - "resgroup-27_mem_swapped": 0,
430 - "resgroup-27_mem_ballooned": 0,
431 - "resgroup-27_mem_overhead": 0,
432 - "resgroup-27_mem_consumed_overhead": 0,
433 - "resgroup-27_mem_compressed": 0,
434 - "resgroup-27_cpu_reservation_used": 0,
435 - "resgroup-27_cpu_max_usage": 4121,
436 - "resgroup-27_cpu_unreserved_for_vm": 4121,
437 - "resgroup-27_mem_reservation_used": 0,
438 - "resgroup-27_mem_max_usage": 1007681536,
439 - "resgroup-27_mem_unreserved_for_vm": 1007681536,
440 - "resgroup-27_cpu_reservation": 4121,
441 - "resgroup-27_cpu_limit": 4121,
442 - "resgroup-27_mem_reservation": 961,
443 - "resgroup-27_mem_limit": 961,
444 - "resgroup-27_overall.status.green": 1,
445 - "resgroup-27_overall.status.gray": 0,
446 - "resgroup-27_overall.status.red": 0,
447 - "resgroup-27_overall.status.yellow": 0,
448 - }
449 -
450 - mx := collr.Collect(context.Background())
451 -
452 - require.Equal(t, expected, mx)
453 -
454 - count := model.Count()
455 - assert.Len(t, collr.discoveredHosts, count.Host)
456 - assert.Len(t, collr.discoveredVMs, count.Machine)
457 - assert.Len(t, collr.discoveredDatastores, count.Datastore)
458 -
459 - numClusters := len(collr.discoveredClusters)
460 - numResourcePools := len(collr.discoveredResourcePools)
461 - assert.Len(t, collr.charted, count.Host+count.Machine+count.Datastore+numClusters+numResourcePools)
462 -
463 - assert.Len(t, *collr.Charts(),
464 - count.Host*len(hostChartsTmpl)+
465 - count.Machine*len(vmChartsTmpl)+
466 - count.Datastore*(len(datastorePropertyChartsTmpl)+len(datastorePerfChartsTmpl))+
467 - numClusters*(len(clusterPropertyChartsTmpl)+len(clusterPerfChartsTmpl))+
468 - numResourcePools*len(resourcePoolChartsTmpl))
469 - collecttest.TestMetricsHasAllChartsDims(t, collr.Charts(), mx)
470 -}
471 -
472 -func TestCollector_Collect_RemoveHostsVMsInRuntime(t *testing.T) {
473 - collr, _, teardown := prepareVSphereSim(t)
474 - defer teardown()
752 +func collectScalarSeriesForTest(t *testing.T, collr *Collector) map[string]metrix.SampleValue {
753 + t.Helper()
754
476 - require.NoError(t, collr.Init(context.Background()))
477 - require.NoError(t, collr.Check(context.Background()))
755 + mx, err := collecttest.CollectScalarSeries(collr, metrix.ReadRaw())
756 + require.NoError(t, err)
757 + if len(mx) == 0 {
758 + return nil
759 + }
760 + return mx
761 +}
762
479 - okHostId := "host-57"
480 - okVmId := "vm-62"
481 - collr.discoverer.(*discover.Discoverer).HostMatcher = mockHostMatcher{okHostId}
482 - collr.discoverer.(*discover.Discoverer).VMMatcher = mockVMMatcher{okVmId}
763 +func buildV2PlanForTest(t *testing.T, collr *Collector) chartengine.Plan {
764 + t.Helper()
765
484 - require.NoError(t, collr.discoverOnce())
766 + engine, err := chartengine.New()
767 + require.NoError(t, err)
768 + require.NoError(t, engine.LoadYAML([]byte(collr.ChartTemplateYAML()), 1))
769 +
770 + reader := collr.MetricStore().Read(metrix.ReadRaw(), metrix.ReadFlatten())
771 + attempt, err := engine.PreparePlan(reader)
772 + require.NoError(t, err)
773 + defer attempt.Abort()
774 +
775 + plan := attempt.Plan()
776 + require.NoError(t, attempt.Commit())
777 + return plan
778 +}
779 +
780 +func requireChartSelectorsMatchSeries(t *testing.T, collr *Collector, contextPrefixes ...string) {
781 + t.Helper()
782 +
783 + spec, err := charttpl.DecodeYAML([]byte(collr.ChartTemplateYAML()))
784 + require.NoError(t, err)
785
486 - numOfRuns := 5
487 - for range numOfRuns {
488 - collr.Collect(context.Background())
786 + reader := collr.MetricStore().Read(metrix.ReadRaw(), metrix.ReadFlatten())
787 + require.NotZero(t, requireChartGroupSelectorsMatchSeries(t, reader, spec.Groups, contextParts(spec.ContextNamespace), contextPrefixes))
788 +}
789 +
790 +func requireChartGroupSelectorsMatchSeries(t *testing.T, reader metrix.Reader, groups []charttpl.Group, parent, contextPrefixes []string) int {
791 + t.Helper()
792 +
793 + matchedContexts := 0
794 + for _, group := range groups {
795 + parts := append(append([]string(nil), parent...), contextParts(group.ContextNamespace)...)
796 + for _, chart := range group.Charts {
797 + contextName := strings.Join(append(append([]string(nil), parts...), strings.TrimSpace(chart.Context)), ".")
798 + if !matchesAnyContextPrefix(contextName, contextPrefixes) {
799 + continue
800 + }
801 + matchedContexts++
802 + for _, dim := range chart.Dimensions {
803 + requireSelectorMatchesSeries(t, reader, contextName, dim.Selector)
804 + }
805 + }
806 + matchedContexts += requireChartGroupSelectorsMatchSeries(t, reader, group.Groups, parts, contextPrefixes)
807 }
808 + return matchedContexts
809 +}
810
491 - host := collr.resources.Hosts.Get(okHostId)
492 - for k, v := range collr.discoveredHosts {
493 - if k == host.ID {
494 - assert.Equal(t, 0, v)
495 - } else {
496 - assert.Equal(t, numOfRuns, v)
811 +func matchesAnyContextPrefix(contextName string, prefixes []string) bool {
812 + for _, prefix := range prefixes {
813 + if contextName == prefix || strings.HasPrefix(contextName, prefix) {
814 + return true
815 }
816 }
817 + return false
818 +}
819
500 - vm := collr.resources.VMs.Get(okVmId)
501 - for id, fails := range collr.discoveredVMs {
502 - if id == vm.ID {
503 - assert.Equal(t, 0, fails)
504 - } else {
505 - assert.Equal(t, numOfRuns, fails)
820 +func requireSelectorMatchesSeries(t *testing.T, reader metrix.Reader, contextName, selector string) {
821 + t.Helper()
822 +
823 + sel, err := metrixselector.Parse(selector)
824 + require.NoErrorf(t, err, "chart context %s selector %q", contextName, selector)
825 +
826 + matched := false
827 + reader.ForEachSeriesIdentity(func(_ metrix.SeriesIdentity, _ metrix.SeriesMeta, metricName string, labels metrix.LabelView, _ metrix.SampleValue) {
828 + if sel.Matches(metricName, labels) {
829 + matched = true
830 }
831 + })
832 + require.Truef(t, matched, "chart context %s selector %q matches no series", contextName, selector)
833 +}
834 +
835 +func contextParts(value string) []string {
836 + value = strings.TrimSpace(value)
837 + if value == "" {
838 + return nil
839 + }
840 + return []string{value}
841 +}
842 +
843 +func v2CreatedChartsAndDims(plan chartengine.Plan) (map[string]chartengine.CreateChartAction, map[string]map[string]chartengine.CreateDimensionAction) {
844 + charts := make(map[string]chartengine.CreateChartAction)
845 + dims := make(map[string]map[string]chartengine.CreateDimensionAction)
846 + for _, action := range plan.Actions {
847 + switch v := action.(type) {
848 + case chartengine.CreateChartAction:
849 + charts[v.ChartID] = v
850 + case chartengine.CreateDimensionAction:
851 + if _, ok := dims[v.ChartID]; !ok {
852 + dims[v.ChartID] = make(map[string]chartengine.CreateDimensionAction)
853 + }
854 + dims[v.ChartID][v.Name] = v
855 + }
856 + }
857 + return charts, dims
858 +}
859 +
860 +func scalarSeriesHasLabel(series map[string]metrix.SampleValue, key, value string) bool {
861 + needle := fmt.Sprintf(`%s="%s"`, key, value)
862 + for name := range series {
863 + if strings.Contains(name, needle) {
864 + return true
865 + }
866 + }
867 + return false
868 +}
869
870 +func runMetricWriteForTest(t *testing.T, collr *Collector, write func()) map[string]metrix.SampleValue {
871 + t.Helper()
872 +
873 + cycle := mustCycleController(t, collr.MetricStore())
874 + cycle.BeginCycle()
875 + write()
876 + require.NoError(t, cycle.CommitCycleSuccess())
877 + return scalarSeriesFromReaderForTest(collr.MetricStore().Read(metrix.ReadRaw()))
878 +}
879 +
880 +func runMetricCollectForTest(t *testing.T, collr *Collector, collect func() error) map[string]metrix.SampleValue {
881 + t.Helper()
882 +
883 + cycle := mustCycleController(t, collr.MetricStore())
884 + cycle.BeginCycle()
885 + if err := collect(); err != nil {
886 + cycle.AbortCycle()
887 + require.NoError(t, err)
888 }
889 + require.NoError(t, cycle.CommitCycleSuccess())
890 + return scalarSeriesFromReaderForTest(collr.MetricStore().Read(metrix.ReadRaw()))
891 +}
892 +
893 +func scalarSeriesFromReaderForTest(reader metrix.Reader) map[string]metrix.SampleValue {
894 + out := make(map[string]metrix.SampleValue)
895 + reader.ForEachSeries(func(name string, labels metrix.LabelView, value metrix.SampleValue) {
896 + out[scalarSeriesKeyForTest(name, labels)] = value
897 + })
898 + return out
899 +}
900
510 - for i := numOfRuns; i < failedUpdatesLimit; i++ {
511 - collr.Collect(context.Background())
901 +func scalarSeriesKeyForTest(name string, labels metrix.LabelView) string {
902 + if labels == nil || labels.Len() == 0 {
903 + return name
904 }
905
514 - assert.Len(t, collr.discoveredHosts, 1)
515 - assert.Len(t, collr.discoveredVMs, 1)
516 - assert.Len(t, collr.charted, 2+len(collr.discoveredDatastores)+len(collr.discoveredClusters)+len(collr.discoveredResourcePools))
906 + var b strings.Builder
907 + b.WriteString(name)
908 + b.WriteByte('{')
909 + first := true
910 + labels.Range(func(key, value string) bool {
911 + if !first {
912 + b.WriteByte(',')
913 + }
914 + first = false
915 + b.WriteString(key)
916 + b.WriteString(`="`)
917 + b.WriteString(value)
918 + b.WriteByte('"')
919 + return true
920 + })
921 + b.WriteByte('}')
922 + return b.String()
923 +}
924 +
925 +func requireScalarSeries(t *testing.T, series map[string]metrix.SampleValue, metric, id string) {
926 + t.Helper()
927 +
928 + _, ok := findScalarSeries(series, metric, id)
929 + require.Truef(t, ok, "expected metric %s with id=%s in %v", metric, id, scalarSeriesKeys(series))
930 +}
931 +
932 +func requireNoScalarSeries(t *testing.T, series map[string]metrix.SampleValue, metric, id string) {
933 + t.Helper()
934 +
935 + _, ok := findScalarSeries(series, metric, id)
936 + require.Falsef(t, ok, "unexpected metric %s with id=%s", metric, id)
937 +}
938 +
939 +func requireScalarSeriesValue(t *testing.T, series map[string]metrix.SampleValue, metric, id string, want int64) {
940 + t.Helper()
941
518 - for _, c := range *collr.Charts() {
519 - if strings.HasPrefix(c.ID, okHostId) || strings.HasPrefix(c.ID, okVmId) ||
520 - strings.HasPrefix(c.ID, "datastore-") || strings.HasPrefix(c.ID, "domain-") || strings.HasPrefix(c.ID, "resgroup-") {
521 - assert.False(t, c.Obsolete)
522 - } else {
523 - assert.True(t, c.Obsolete)
942 + got, ok := findScalarSeries(series, metric, id)
943 + require.Truef(t, ok, "expected metric %s with id=%s in %v", metric, id, scalarSeriesKeys(series))
944 + require.EqualValues(t, want, got)
945 +}
946 +
947 +func findScalarSeries(series map[string]metrix.SampleValue, metric, id string) (metrix.SampleValue, bool) {
948 + prefix := metric + "{"
949 + idLabel := fmt.Sprintf(`id="%s"`, id)
950 + for key, value := range series {
951 + if (key == metric || strings.HasPrefix(key, prefix)) && strings.Contains(key, idLabel) {
952 + return value, true
953 }
954 }
955 + return 0, false
956 +}
957 +
958 +func scalarSeriesKeys(series map[string]metrix.SampleValue) []string {
959 + keys := make([]string, 0, len(series))
960 + for key := range series {
961 + keys = append(keys, key)
962 + }
963 + return keys
964 +}
965 +
966 +func firstResourceID(collr *Collector) string {
967 + for id := range collr.resources.Hosts {
968 + return id
969 + }
970 + for id := range collr.resources.VMs {
971 + return id
972 + }
973 + return ""
974 +}
975 +
976 +func mustCycleController(t *testing.T, store metrix.CollectorStore) metrix.CycleController {
977 + t.Helper()
978 +
979 + managed, ok := metrix.AsCycleManagedStore(store)
980 + require.True(t, ok)
981 + return managed.CycleController()
982 }
983
984 func TestCollector_Collect_Run(t *testing.T) {
529 - collr, model, teardown := prepareVSphereSim(t)
985 + collr, _, teardown := prepareVSphereSim(t)
986 defer teardown()
987
988 collr.DiscoveryInterval = confopt.Duration(time.Second * 2)
@@ -535,26 +991,12 @@ func TestCollector_Collect_Run(t *testing.T) {
991
992 runs := 20
993 for i := range runs {
538 - assert.True(t, len(collr.Collect(context.Background())) > 0)
994 + assert.True(t, len(collectScalarSeriesForTest(t, collr)) > 0)
995 if i < 6 {
996 time.Sleep(time.Second)
997 }
998 }
543 -
544 - count := model.Count()
545 - assert.Len(t, collr.discoveredHosts, count.Host)
546 - assert.Len(t, collr.discoveredVMs, count.Machine)
547 - assert.Len(t, collr.discoveredDatastores, count.Datastore)
548 -
549 - numClusters := len(collr.discoveredClusters)
550 - numResourcePools := len(collr.discoveredResourcePools)
551 - assert.Len(t, collr.charted, count.Host+count.Machine+count.Datastore+numClusters+numResourcePools)
552 - assert.Len(t, *collr.charts,
553 - count.Host*len(hostChartsTmpl)+
554 - count.Machine*len(vmChartsTmpl)+
555 - count.Datastore*(len(datastorePropertyChartsTmpl)+len(datastorePerfChartsTmpl))+
556 - numClusters*(len(clusterPropertyChartsTmpl)+len(clusterPerfChartsTmpl))+
557 - numResourcePools*len(resourcePoolChartsTmpl))
999 + collecttest.AssertChartCoverage(t, collr, collecttest.ChartCoverageExpectation{})
1000 }
1001
1002 func prepareVSphereSim(t *testing.T) (collr *Collector, model *simulator.Model, teardown func()) {
@@ -619,6 +1061,22 @@ type mockVMMatcher struct{ name string }
1061 func (m mockHostMatcher) Match(host *rs.Host) bool { return m.name == host.ID }
1062 func (m mockVMMatcher) Match(vm *rs.VM) bool { return m.name == vm.ID }
1063
1064 +type mockScraperNoHostPerf struct {
1065 + mockScraper
1066 +}
1067 +
1068 +func (s mockScraperNoHostPerf) ScrapeHosts(rs.Hosts) []performance.EntityMetric {
1069 + return nil
1070 +}
1071 +
1072 +type mockScraperNoVMPerf struct {
1073 + mockScraper
1074 +}
1075 +
1076 +func (s mockScraperNoVMPerf) ScrapeVMs(rs.VMs) []performance.EntityMetric {
1077 + return nil
1078 +}
1079 +
1080 // mockScraperNoDSPerf wraps a scraper but returns no perf data for datastores (simulates vSAN).
1081 type mockScraperNoDSPerf struct {
1082 scraper
@@ -637,72 +1095,6 @@ func (s mockScraperNoDSPerf) ScrapeClusters(clusters rs.Clusters) []performance.
1095 return populateMetrics(s.scraper.ScrapeClusters(clusters), 400)
1096 }
1097
640 -func TestCollector_Collect_DatastoreNoPerfData(t *testing.T) {
641 - collr, model, teardown := prepareVSphereSim(t)
642 - defer teardown()
643 -
644 - require.NoError(t, collr.Init(context.Background()))
645 -
646 - collr.scraper = mockScraperNoDSPerf{collr.scraper}
647 -
648 - mx := collr.Collect(context.Background())
649 - require.NotNil(t, mx)
650 -
651 - count := model.Count()
652 -
653 - // Property metrics should be present.
654 - for _, ds := range collr.resources.Datastores {
655 - assert.Contains(t, mx, ds.ID+"_capacity")
656 - assert.Contains(t, mx, ds.ID+"_free_space")
657 - assert.Contains(t, mx, ds.ID+"_used_space")
658 - assert.Contains(t, mx, ds.ID+"_used_space_pct")
659 - assert.Contains(t, mx, ds.ID+"_overall.status.green")
660 - }
661 -
662 - // Perf metrics should NOT be present.
663 - for _, ds := range collr.resources.Datastores {
664 - assert.NotContains(t, mx, ds.ID+"_datastore.read.average")
665 - assert.NotContains(t, mx, ds.ID+"_datastore.numberReadAveraged.average")
666 - }
667 -
668 - numClusters := len(collr.discoveredClusters)
669 - numResourcePools := len(collr.discoveredResourcePools)
670 -
671 - // Only property charts created for datastores, no perf charts.
672 - assert.Len(t, *collr.Charts(),
673 - count.Host*len(hostChartsTmpl)+
674 - count.Machine*len(vmChartsTmpl)+
675 - count.Datastore*len(datastorePropertyChartsTmpl)+
676 - numClusters*(len(clusterPropertyChartsTmpl)+len(clusterPerfChartsTmpl))+
677 - numResourcePools*len(resourcePoolChartsTmpl))
678 -
679 - // datastorePerfReceived should be empty.
680 - assert.Empty(t, collr.datastorePerfReceived)
681 - assert.Empty(t, collr.datastorePerfCharted)
682 -
683 - // Now switch to a scraper that returns perf data — perf charts should appear.
684 - collr.scraper = mockScraper{collr.scraper.(mockScraperNoDSPerf).scraper}
685 -
686 - mx = collr.Collect(context.Background())
687 - require.NotNil(t, mx)
688 -
689 - // Perf metrics should now be present.
690 - for _, ds := range collr.resources.Datastores {
691 - assert.Contains(t, mx, ds.ID+"_datastore.read.average")
692 - }
693 -
694 - // Both property and perf charts now for datastores.
695 - assert.Len(t, *collr.Charts(),
696 - count.Host*len(hostChartsTmpl)+
697 - count.Machine*len(vmChartsTmpl)+
698 - count.Datastore*(len(datastorePropertyChartsTmpl)+len(datastorePerfChartsTmpl))+
699 - numClusters*(len(clusterPropertyChartsTmpl)+len(clusterPerfChartsTmpl))+
700 - numResourcePools*len(resourcePoolChartsTmpl))
701 -
702 - assert.Len(t, collr.datastorePerfReceived, count.Datastore)
703 - assert.Len(t, collr.datastorePerfCharted, count.Datastore)
704 -}
705 -
1098 // mockScraperNoClusterPerf wraps a scraper but returns no perf data for clusters.
1099 type mockScraperNoClusterPerf struct {
1100 scraper
@@ -721,177 +1113,82 @@ func (s mockScraperNoClusterPerf) ScrapeClusters(_ rs.Clusters) []performance.En
1113 return nil
1114 }
1115
724 -func TestCollector_Collect_ClusterNoPerfData(t *testing.T) {
725 - collr, model, teardown := prepareVSphereSim(t)
726 - defer teardown()
727 -
728 - require.NoError(t, collr.Init(context.Background()))
729 -
730 - collr.scraper = mockScraperNoClusterPerf{collr.scraper}
731 -
732 - mx := collr.Collect(context.Background())
733 - require.NotNil(t, mx)
734 -
735 - count := model.Count()
736 -
737 - // Cluster property metrics should be present.
738 - for _, cl := range collr.resources.Clusters {
739 - assert.Contains(t, mx, cl.ID+"_num_hosts")
740 - assert.Contains(t, mx, cl.ID+"_total_cpu")
741 - assert.Contains(t, mx, cl.ID+"_overall.status.green")
742 - }
743 -
744 - // Cluster perf metrics should NOT be present.
745 - for _, cl := range collr.resources.Clusters {
746 - assert.NotContains(t, mx, cl.ID+"_cpu.usage.average")
747 - assert.NotContains(t, mx, cl.ID+"_clusterServices.cpufairness.latest")
748 - }
749 -
750 - numClusters := len(collr.discoveredClusters)
751 - numResourcePools := len(collr.discoveredResourcePools)
752 -
753 - // Only property charts created for clusters, no perf charts.
754 - assert.Len(t, *collr.Charts(),
755 - count.Host*len(hostChartsTmpl)+
756 - count.Machine*len(vmChartsTmpl)+
757 - count.Datastore*(len(datastorePropertyChartsTmpl)+len(datastorePerfChartsTmpl))+
758 - numClusters*len(clusterPropertyChartsTmpl)+
759 - numResourcePools*len(resourcePoolChartsTmpl))
760 -
761 - // clusterPerfReceived should be empty.
762 - assert.Empty(t, collr.clusterPerfReceived)
763 - assert.Empty(t, collr.clusterPerfCharted)
764 -
765 - // Now switch to a scraper that returns perf data — perf charts should appear.
766 - collr.scraper = mockScraper{collr.scraper.(mockScraperNoClusterPerf).scraper}
767 -
768 - mx = collr.Collect(context.Background())
769 - require.NotNil(t, mx)
770 -
771 - // Perf metrics should now be present.
772 - for _, cl := range collr.resources.Clusters {
773 - assert.Contains(t, mx, cl.ID+"_cpu.usage.average")
774 - }
775 -
776 - // Both property and perf charts now for clusters.
777 - assert.Len(t, *collr.Charts(),
778 - count.Host*len(hostChartsTmpl)+
779 - count.Machine*len(vmChartsTmpl)+
780 - count.Datastore*(len(datastorePropertyChartsTmpl)+len(datastorePerfChartsTmpl))+
781 - numClusters*(len(clusterPropertyChartsTmpl)+len(clusterPerfChartsTmpl))+
782 - numResourcePools*len(resourcePoolChartsTmpl))
783 -
784 - assert.Len(t, collr.clusterPerfReceived, numClusters)
785 - assert.Len(t, collr.clusterPerfCharted, numClusters)
786 -}
787 -
788 -func TestCollector_Collect_ClusterEvictionCleansUpMaps(t *testing.T) {
789 - collr, _, teardown := prepareVSphereSim(t)
790 - defer teardown()
791 -
792 - require.NoError(t, collr.Init(context.Background()))
793 -
794 - collr.scraper = mockScraper{collr.scraper}
795 -
796 - // First collect — creates all charts including cluster perf charts.
797 - mx := collr.Collect(context.Background())
798 - require.NotNil(t, mx)
799 -
800 - assert.NotEmpty(t, collr.discoveredClusters)
801 - assert.NotEmpty(t, collr.clusterPerfReceived)
802 - assert.NotEmpty(t, collr.clusterPerfCharted)
803 -
804 - // Simulate eviction: disable property collector and perf scraper so the counter isn't reset,
805 - // then set the failure counter to the eviction threshold.
806 - collr.clusterPropertyCollector = nil
807 - collr.scraper = mockScraperNoClusterPerf{collr.scraper.(mockScraper).scraper}
808 - for id := range collr.discoveredClusters {
809 - collr.discoveredClusters[id] = failedUpdatesLimit
1116 +func TestCollector_Collect_NoPerfData(t *testing.T) {
1117 + tests := map[string]struct {
1118 + setNoPerfScraper func(*Collector)
1119 + restoreScraper func(*Collector)
1120 + checkNoPerf func(*testing.T, *Collector, map[string]metrix.SampleValue, simulator.Model)
1121 + checkWithPerf func(*testing.T, *Collector, map[string]metrix.SampleValue, simulator.Model)
1122 + }{
1123 + "datastores": {
1124 + setNoPerfScraper: func(c *Collector) { c.scraper = mockScraperNoDSPerf{c.scraper} },
1125 + restoreScraper: func(c *Collector) { c.scraper = mockScraper{c.scraper.(mockScraperNoDSPerf).scraper} },
1126 + checkNoPerf: func(t *testing.T, collr *Collector, series map[string]metrix.SampleValue, count simulator.Model) {
1127 + for _, ds := range collr.resources.Datastores {
1128 + requireScalarSeries(t, series, "datastore_space_usage_capacity", ds.ID)
1129 + requireScalarSeries(t, series, "datastore_space_usage_free", ds.ID)
1130 + requireScalarSeries(t, series, "datastore_space_usage_used", ds.ID)
1131 + requireScalarSeries(t, series, "datastore_space_utilization_used", ds.ID)
1132 + requireScalarSeries(t, series, "datastore_space_usage_uncommitted", ds.ID)
1133 + requireScalarSeries(t, series, "datastore_overall_status_green", ds.ID)
1134 + requireScalarSeries(t, series, "datastore_accessibility_status_accessible", ds.ID)
1135 + requireScalarSeries(t, series, "datastore_maintenance_status_normal", ds.ID)
1136 + requireScalarSeries(t, series, "datastore_multiple_host_access_unknown", ds.ID)
1137 + requireNoScalarSeries(t, series, "datastore_disk_io_read", ds.ID)
1138 + requireNoScalarSeries(t, series, "datastore_disk_iops_reads", ds.ID)
1139 + }
1140 +
1141 + collecttest.AssertChartCoverage(t, collr, collecttest.ChartCoverageExpectation{})
1142 + },
1143 + checkWithPerf: func(t *testing.T, collr *Collector, series map[string]metrix.SampleValue, count simulator.Model) {
1144 + for _, ds := range collr.resources.Datastores {
1145 + requireScalarSeries(t, series, "datastore_disk_io_read", ds.ID)
1146 + }
1147 +
1148 + collecttest.AssertChartCoverage(t, collr, collecttest.ChartCoverageExpectation{})
1149 + },
1150 + },
1151 + "clusters": {
1152 + setNoPerfScraper: func(c *Collector) { c.scraper = mockScraperNoClusterPerf{c.scraper} },
1153 + restoreScraper: func(c *Collector) { c.scraper = mockScraper{c.scraper.(mockScraperNoClusterPerf).scraper} },
1154 + checkNoPerf: func(t *testing.T, collr *Collector, series map[string]metrix.SampleValue, count simulator.Model) {
1155 + for _, cl := range collr.resources.Clusters {
1156 + requireScalarSeries(t, series, "cluster_hosts_total", cl.ID)
1157 + requireScalarSeries(t, series, "cluster_cpu_capacity_total", cl.ID)
1158 + requireScalarSeries(t, series, "cluster_overall_status_green", cl.ID)
1159 + requireNoScalarSeries(t, series, "cluster_cpu_utilization_used", cl.ID)
1160 + requireNoScalarSeries(t, series, "cluster_services_fairness_cpu", cl.ID)
1161 + }
1162 +
1163 + collecttest.AssertChartCoverage(t, collr, collecttest.ChartCoverageExpectation{})
1164 + },
1165 + checkWithPerf: func(t *testing.T, collr *Collector, series map[string]metrix.SampleValue, count simulator.Model) {
1166 + for _, cl := range collr.resources.Clusters {
1167 + requireScalarSeries(t, series, "cluster_cpu_utilization_used", cl.ID)
1168 + }
1169 +
1170 + collecttest.AssertChartCoverage(t, collr, collecttest.ChartCoverageExpectation{})
1171 + },
1172 + },
1173 }
1174
812 - // Next collect increments counter past the limit, triggering eviction in updateCharts.
813 - collr.Collect(context.Background())
1175 + for name, tc := range tests {
1176 + t.Run(name, func(t *testing.T) {
1177 + collr, model, teardown := prepareVSphereSim(t)
1178 + defer teardown()
1179
815 - assert.Empty(t, collr.discoveredClusters)
816 - assert.Empty(t, collr.clusterPerfReceived)
817 - assert.Empty(t, collr.clusterPerfCharted)
1180 + require.NoError(t, collr.Init(context.Background()))
1181 + tc.setNoPerfScraper(collr)
1182
819 - // Cluster charts should be marked obsolete.
820 - for _, c := range *collr.Charts() {
821 - if strings.HasPrefix(c.ID, "domain-") {
822 - assert.True(t, c.Obsolete, "chart %s should be obsolete", c.ID)
823 - }
824 - }
825 -}
1183 + mx := collectScalarSeriesForTest(t, collr)
1184 + require.NotNil(t, mx)
1185 + count := model.Count()
1186 + tc.checkNoPerf(t, collr, mx, count)
1187
827 -func TestCollector_Collect_ResourcePoolEvictionCleansUpMaps(t *testing.T) {
828 - collr, _, teardown := prepareVSphereSim(t)
829 - defer teardown()
830 -
831 - require.NoError(t, collr.Init(context.Background()))
832 -
833 - collr.scraper = mockScraper{collr.scraper}
834 -
835 - // First collect — creates resource pool charts.
836 - mx := collr.Collect(context.Background())
837 - require.NotNil(t, mx)
838 -
839 - assert.NotEmpty(t, collr.discoveredResourcePools)
840 -
841 - // Simulate eviction: disable property collector so the counter isn't reset.
842 - collr.rpPropertyCollector = nil
843 - for id := range collr.discoveredResourcePools {
844 - collr.discoveredResourcePools[id] = failedUpdatesLimit
845 - }
846 -
847 - // Next collect increments counter past the limit, triggering eviction.
848 - collr.Collect(context.Background())
849 -
850 - assert.Empty(t, collr.discoveredResourcePools)
851 -
852 - // Resource pool charts should be marked obsolete.
853 - for _, c := range *collr.Charts() {
854 - if strings.HasPrefix(c.ID, "resgroup-") {
855 - assert.True(t, c.Obsolete, "chart %s should be obsolete", c.ID)
856 - }
857 - }
858 -}
859 -
860 -func TestCollector_Collect_DatastoreEvictionCleansUpMaps(t *testing.T) {
861 - collr, _, teardown := prepareVSphereSim(t)
862 - defer teardown()
863 -
864 - require.NoError(t, collr.Init(context.Background()))
865 -
866 - collr.scraper = mockScraper{collr.scraper}
867 -
868 - // First collect — creates all charts including datastore perf charts.
869 - mx := collr.Collect(context.Background())
870 - require.NotNil(t, mx)
871 -
872 - assert.NotEmpty(t, collr.discoveredDatastores)
873 - assert.NotEmpty(t, collr.datastorePerfReceived)
874 - assert.NotEmpty(t, collr.datastorePerfCharted)
875 -
876 - // Simulate eviction: disable property collector and perf scraper so the counter isn't reset,
877 - // then set the failure counter to the eviction threshold.
878 - collr.dsPropertyCollector = nil
879 - collr.scraper = mockScraperNoDSPerf{collr.scraper.(mockScraper).scraper}
880 - for id := range collr.discoveredDatastores {
881 - collr.discoveredDatastores[id] = failedUpdatesLimit
882 - }
883 -
884 - // Next collect increments counter past the limit, triggering eviction in updateCharts.
885 - collr.Collect(context.Background())
886 -
887 - assert.Empty(t, collr.discoveredDatastores)
888 - assert.Empty(t, collr.datastorePerfReceived)
889 - assert.Empty(t, collr.datastorePerfCharted)
890 -
891 - // Datastore charts should be marked obsolete.
892 - for _, c := range *collr.Charts() {
893 - if strings.HasPrefix(c.ID, "datastore-") {
894 - assert.True(t, c.Obsolete, "chart %s should be obsolete", c.ID)
895 - }
1188 + tc.restoreScraper(collr)
1189 + mx = collectScalarSeriesForTest(t, collr)
1190 + require.NotNil(t, mx)
1191 + tc.checkWithPerf(t, collr, mx, count)
1192 + })
1193 }
1194 }
src/go/plugin/go.d/collector/vsphere/config_schema.json
+189 -36
@@ -38,6 +38,122 @@
38 "minimum": 60,
39 "default": 300
40 },
41 + "tag_categories": {
42 + "title": "vSphere tag categories",
43 + "description": "Optional glob-pattern allowlist of vSphere tag category names to expose as chart labels. Each list item is one pattern, so names with spaces are supported. Disabled by default because tags are user-defined metadata and can expose internal names. Use \"*\" only when all tag categories are intentional. Label keys are vsphere_tag_<sanitized_category>; multiple tags in the same category are sorted and joined with \"|\".",
44 + "type": [
45 + "array",
46 + "null"
47 + ],
48 + "uniqueItems": true,
49 + "items": {
50 + "title": "vSphere tag category",
51 + "type": "string",
52 + "default": "Environment"
53 + },
54 + "default": []
55 + },
56 + "custom_attributes": {
57 + "title": "vSphere custom attributes",
58 + "description": "Optional glob-pattern allowlist of vSphere custom attribute names to expose as chart labels. Each list item is one pattern, so names with spaces are supported. Disabled by default because custom attributes are user-defined metadata and can expose internal names, ownership data, or secrets stored by administrators. Custom attribute values are sent verbatim as labels; never enable patterns that can match secret values. Use \"*\" only when all custom attributes are intentional. Label keys are vsphere_custom_attribute_<sanitized_name>.",
59 + "type": [
60 + "array",
61 + "null"
62 + ],
63 + "uniqueItems": true,
64 + "items": {
65 + "title": "vSphere custom attribute",
66 + "type": "string",
67 + "default": "Owner"
68 + },
69 + "default": []
70 + },
71 + "collect_datastore_clusters": {
72 + "title": "Collect datastore clusters",
73 + "description": "If enabled, collects aggregate capacity and Storage DRS status for datastore clusters (StoragePod objects). Disabled by default because it adds another discovered resource class.",
74 + "type": "boolean",
75 + "default": false
76 + },
77 + "datastore_cluster_include": {
78 + "title": "Datastore cluster selector",
79 + "description": "Simple-pattern allowlist for discovered datastore clusters when collect_datastore_clusters is enabled. Matching datastore clusters are included in metrics, labels, cached discovery state, and topology. Patterns match /Datacenter/DatastoreCluster, the datastore cluster name, or the vSphere managed object ID.",
80 + "type": [
81 + "array",
82 + "null"
83 + ],
84 + "uniqueItems": true,
85 + "items": {
86 + "title": "Datastore cluster selector",
87 + "type": "string",
88 + "default": "/*"
89 + },
90 + "default": [
91 + "/*"
92 + ]
93 + },
94 + "collect_vsan": {
95 + "title": "Collect vSAN metrics",
96 + "description": "If enabled, collects vSAN cluster capacity, vSAN cluster health, and vSAN cluster, host, and VM performance metrics through the vSAN Management API. Disabled by default because it requires vSAN APIs and the vSAN Performance Service, and adds additional vCenter queries. Use the vSAN selectors to choose the concrete vSAN performance entity refs queried.",
97 + "type": "boolean",
98 + "default": false
99 + },
100 + "vsan_cluster_include": {
101 + "title": "vSAN cluster selector",
102 + "description": "Simple-pattern allowlist for vSAN clusters when collect_vsan is enabled. Patterns match /Datacenter/Cluster, the cluster name, the vSphere managed object ID, or vsan_uuid:<uuid>.",
103 + "type": [
104 + "array",
105 + "null"
106 + ],
107 + "uniqueItems": true,
108 + "items": {
109 + "title": "vSAN cluster selector",
110 + "type": "string",
111 + "default": "/*"
112 + },
113 + "default": [
114 + "/*"
115 + ]
116 + },
117 + "vsan_host_include": {
118 + "title": "vSAN host selector",
119 + "description": "Simple-pattern allowlist for vSAN host performance entities when collect_vsan is enabled. Patterns match /Datacenter/Cluster/Host, the host name, the vSphere managed object ID, or vsan_node_uuid:<uuid>.",
120 + "type": [
121 + "array",
122 + "null"
123 + ],
124 + "uniqueItems": true,
125 + "items": {
126 + "title": "vSAN host selector",
127 + "type": "string",
128 + "default": "/*"
129 + },
130 + "default": [
131 + "/*"
132 + ]
133 + },
134 + "vsan_vm_include": {
135 + "title": "vSAN VM selector",
136 + "description": "Simple-pattern allowlist for vSAN VM performance entities when collect_vsan is enabled. Patterns match /Datacenter/Cluster/Host/VM, the VM name, the vSphere managed object ID, or instance_uuid:<uuid>.",
137 + "type": [
138 + "array",
139 + "null"
140 + ],
141 + "uniqueItems": true,
142 + "items": {
143 + "title": "vSAN VM selector",
144 + "type": "string",
145 + "default": "/*"
146 + },
147 + "default": [
148 + "/*"
149 + ]
150 + },
151 + "collect_network_topology": {
152 + "title": "Collect network topology",
153 + "description": "If enabled, discovers vSphere Network and Distributed Virtual Port Group objects for the cached vSphere Topology function. Disabled by default to avoid extra vCenter discovery calls for existing users.",
154 + "type": "boolean",
155 + "default": false
156 + },
157 "not_follow_redirects": {
158 "title": "Not follow redirects",
159 "description": "If set, the client will not follow HTTP redirects automatically.",
@@ -60,8 +176,8 @@
176 "title": "Host selector",
177 "description": "",
178 "type": "string",
63 - "default": "/*/*/*",
64 - "pattern": "^$|^/"
179 + "default": "/*",
180 + "pattern": "^/"
181 },
182 "default": [
183 "/*"
@@ -79,8 +195,8 @@
195 "title": "VM selector",
196 "description": "",
197 "type": "string",
82 - "default": "/*/*/*/*",
83 - "pattern": "^$|^/"
198 + "default": "/*",
199 + "pattern": "^/"
200 },
201 "default": [
202 "/*"
@@ -98,8 +214,8 @@
214 "title": "Datastore selector",
215 "description": "",
216 "type": "string",
101 - "default": "/*/*",
102 - "pattern": "^$|^/"
217 + "default": "/*",
218 + "pattern": "^/"
219 },
220 "default": [
221 "/*"
@@ -117,8 +233,8 @@
233 "title": "Cluster selector",
234 "description": "",
235 "type": "string",
120 - "default": "/*/*",
121 - "pattern": "^$|^/"
236 + "default": "/*",
237 + "pattern": "^/"
238 },
239 "default": [
240 "/*"
@@ -209,11 +325,7 @@
325 "required": [
326 "url",
327 "username",
212 - "password",
213 - "host_include",
214 - "vm_include",
215 - "datastore_include",
216 - "cluster_include"
328 + "password"
329 ]
330 },
331 "uiSchema": {
@@ -231,12 +343,11 @@
343 "url",
344 "timeout",
345 "discovery_interval",
234 - "not_follow_redirects",
346 "vnode"
347 ]
348 },
349 {
239 - "title": "Hosts, VMs, Datastores & Clusters selector",
350 + "title": "Filters",
351 "fields": [
352 "host_include",
353 "vm_include",
@@ -245,33 +356,48 @@
356 ]
357 },
358 {
248 - "title": "Auth",
359 + "title": "Labels",
360 "fields": [
250 - "username",
251 - "password"
361 + "tag_categories",
362 + "custom_attributes"
363 ]
364 },
365 {
255 - "title": "TLS",
366 + "title": "Stores",
367 "fields": [
257 - "tls_skip_verify",
258 - "tls_ca",
259 - "tls_cert",
260 - "tls_key"
368 + "collect_datastore_clusters",
369 + "datastore_cluster_include"
370 ]
371 },
372 {
264 - "title": "Proxy",
373 + "title": "vSAN",
374 "fields": [
266 - "proxy_url",
267 - "proxy_username",
268 - "proxy_password"
375 + "collect_vsan",
376 + "vsan_cluster_include",
377 + "vsan_host_include",
378 + "vsan_vm_include"
379 ]
380 },
381 {
272 - "title": "Headers",
382 + "title": "Topology",
383 "fields": [
274 - "headers"
384 + "collect_network_topology"
385 + ]
386 + },
387 + {
388 + "title": "Auth",
389 + "fields": [
390 + "username",
391 + "password"
392 + ]
393 + },
394 + {
395 + "title": "TLS",
396 + "fields": [
397 + "tls_skip_verify",
398 + "tls_ca",
399 + "tls_cert",
400 + "tls_key"
401 ]
402 }
403 ]
@@ -289,6 +415,21 @@
415 "force_http2": {
416 "ui:widget": "hidden"
417 },
418 + "not_follow_redirects": {
419 + "ui:widget": "hidden"
420 + },
421 + "proxy_url": {
422 + "ui:widget": "hidden"
423 + },
424 + "proxy_username": {
425 + "ui:widget": "hidden"
426 + },
427 + "proxy_password": {
428 + "ui:widget": "hidden"
429 + },
430 + "headers": {
431 + "ui:widget": "hidden"
432 + },
433 "autodetection_retry": {
434 "ui:help": "This option determines how frequently (in seconds) Netdata will retry data collection jobs that failed initially, with the value of 60 meaning it retries to start data collection jobs every 60 seconds, while setting it to 0 disables this retry mechanism entirely."
435 },
@@ -299,7 +440,7 @@
440 "ui:help": "**Important**: vSphere generates real-time statistics every 20 seconds. Setting this value lower won't improve data accuracy. For larger vSphere deployments, consider increasing this value to ensure complete data collection during each cycle. To find the optimal value, run the collector in debug mode and see how long it takes to collect metrics."
441 },
442 "url": {
302 - "ui:placeholder": "https://203.0.113.0"
443 + "ui:placeholder": "https://vcenter.local"
444 },
445 "timeout": {
446 "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
@@ -310,6 +451,24 @@
451 "vm_include": {
452 "ui:listFlavour": "list"
453 },
454 + "tag_categories": {
455 + "ui:listFlavour": "list"
456 + },
457 + "custom_attributes": {
458 + "ui:listFlavour": "list"
459 + },
460 + "vsan_cluster_include": {
461 + "ui:listFlavour": "list"
462 + },
463 + "vsan_host_include": {
464 + "ui:listFlavour": "list"
465 + },
466 + "vsan_vm_include": {
467 + "ui:listFlavour": "list"
468 + },
469 + "datastore_cluster_include": {
470 + "ui:listFlavour": "list"
471 + },
472 "datastore_include": {
473 "ui:listFlavour": "list"
474 },
@@ -320,14 +479,8 @@
479 "ui:placeholder": "admin@vsphere.local",
480 "ui:widget": "password"
481 },
323 - "proxy_username": {
324 - "ui:widget": "password"
325 - },
482 "password": {
483 "ui:widget": "password"
328 - },
329 - "proxy_password": {
330 - "ui:widget": "password"
484 }
485 }
486 }
src/go/plugin/go.d/collector/vsphere/datastore_clusters.go new
+63
@@ -0,0 +1,63 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/metrix"
7 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
8 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/oldmetrix"
9 +)
10 +
11 +const (
12 + datastoreClusterSpaceUsageCapacityMetric = "datastore_cluster_space_usage_capacity"
13 + datastoreClusterSpaceUsageFreeMetric = "datastore_cluster_space_usage_free"
14 + datastoreClusterSpaceUsageUsedMetric = "datastore_cluster_space_usage_used"
15 + datastoreClusterSpaceUtilizationUsedMetric = "datastore_cluster_space_utilization_used"
16 + datastoreClusterStorageDRSEnabledMetric = "datastore_cluster_storage_drs_status_enabled"
17 + datastoreClusterStorageDRSDisabledMetric = "datastore_cluster_storage_drs_status_disabled"
18 + datastoreClusterOverallStatusGreenMetric = "datastore_cluster_overall_status_green"
19 + datastoreClusterOverallStatusRedMetric = "datastore_cluster_overall_status_red"
20 + datastoreClusterOverallStatusYellowMetric = "datastore_cluster_overall_status_yellow"
21 + datastoreClusterOverallStatusGrayMetric = "datastore_cluster_overall_status_gray"
22 + datastoreClusterNameLabel = "datastore_cluster"
23 +)
24 +
25 +func (c *Collector) writeDatastoreClusterMetrics() {
26 + if !c.CollectDatastoreClusters || c.resources == nil {
27 + return
28 + }
29 +
30 + for _, pod := range sortedStoragePods(c.resources.StoragePods) {
31 + labels := c.labelSet(c.datastoreClusterLabels(pod))
32 + c.observeGauge(datastoreClusterSpaceUsageCapacityMetric, pod.Capacity, labels)
33 + c.observeGauge(datastoreClusterSpaceUsageFreeMetric, pod.FreeSpace, labels)
34 + used := max(pod.Capacity-pod.FreeSpace, 0)
35 + c.observeGauge(datastoreClusterSpaceUsageUsedMetric, used, labels)
36 + if pod.Capacity > 0 {
37 + c.observeGauge(datastoreClusterSpaceUtilizationUsedMetric, int64(float64(used)/float64(pod.Capacity)*scaledPercent), labels)
38 + } else {
39 + c.observeGauge(datastoreClusterSpaceUtilizationUsedMetric, 0, labels)
40 + }
41 + c.observeGauge(datastoreClusterStorageDRSEnabledMetric, oldmetrix.Bool(pod.StorageDRSEnabled != nil && *pod.StorageDRSEnabled), labels)
42 + c.observeGauge(datastoreClusterStorageDRSDisabledMetric, oldmetrix.Bool(pod.StorageDRSEnabled != nil && !*pod.StorageDRSEnabled), labels)
43 + status := pod.OverallStatus
44 + if status == "" {
45 + status = "gray"
46 + }
47 + c.observeGauge(datastoreClusterOverallStatusGreenMetric, oldmetrix.Bool(status == "green"), labels)
48 + c.observeGauge(datastoreClusterOverallStatusRedMetric, oldmetrix.Bool(status == "red"), labels)
49 + c.observeGauge(datastoreClusterOverallStatusYellowMetric, oldmetrix.Bool(status == "yellow"), labels)
50 + c.observeGauge(datastoreClusterOverallStatusGrayMetric, oldmetrix.Bool(status == "gray"), labels)
51 + }
52 +}
53 +
54 +func (c *Collector) datastoreClusterLabels(pod *rs.StoragePod) []metrix.Label {
55 + return c.v2MetricLabels(pod.ID, datastoreClusterLabels(pod), pod.Labels)
56 +}
57 +
58 +func datastoreClusterLabels(pod *rs.StoragePod) []metrix.Label {
59 + return []metrix.Label{
60 + {Key: "datacenter", Value: pod.Hier.DC.Name},
61 + {Key: datastoreClusterNameLabel, Value: pod.Name},
62 + }
63 +}
src/go/plugin/go.d/collector/vsphere/datastore_clusters_test.go new
+204
@@ -0,0 +1,204 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import (
6 + "context"
7 + "testing"
8 +
9 + "github.com/stretchr/testify/require"
10 +
11 + "github.com/netdata/netdata/go/plugins/pkg/metrix"
12 + "github.com/netdata/netdata/go/plugins/plugin/framework/chartengine"
13 + "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/match"
14 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
15 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
16 +)
17 +
18 +func TestCollector_Init_ReturnsFalseIfInvalidDatastoreClusterConfig(t *testing.T) {
19 + collr := New()
20 + collr.URL = "https://vcenter.local"
21 + collr.Username = "user"
22 + collr.Password = "pass"
23 + collr.CollectDatastoreClusters = true
24 +
25 + collr.DatastoreClustersInclude = match.DatastoreClusterIncludes{"["}
26 + require.ErrorContains(t, collr.Init(context.Background()), "datastore_cluster_include has invalid pattern")
27 +}
28 +
29 +func TestCollector_DatastoreClustersDefaultOff(t *testing.T) {
30 + collr, _, teardown := prepareVSphereSim(t)
31 + defer teardown()
32 +
33 + require.NoError(t, collr.Init(context.Background()))
34 + collr.scraper = mockScraper{collr.scraper}
35 + setOnlyTestStoragePods(collr, []*rs.StoragePod{testStoragePod("group-p1", "DC0_POD0", 1000, 400, true)})
36 +
37 + require.NotEmpty(t, collectScalarSeriesForTest(t, collr))
38 +
39 + require.Zero(t, countMetricSeries(collr.MetricStore().Read(metrix.ReadRaw()), datastoreClusterSpaceUsageCapacityMetric))
40 +}
41 +
42 +func TestCollector_DatastoreClustersOptInEmitsCharts(t *testing.T) {
43 + collr, _, teardown := prepareVSphereSim(t)
44 + defer teardown()
45 + collr.CollectDatastoreClusters = true
46 +
47 + require.NoError(t, collr.Init(context.Background()))
48 + collr.scraper = mockScraper{collr.scraper}
49 + pod := testStoragePod("group-p1", "DC0_POD0", 1000, 400, true)
50 + pod.Labels = map[string]string{"vsphere_tag_environment": "prod"}
51 + setOnlyTestStoragePods(collr, []*rs.StoragePod{pod})
52 +
53 + require.NotEmpty(t, collectScalarSeriesForTest(t, collr))
54 +
55 + labels := datastoreClusterLabelsMap(pod)
56 + labels["vsphere_tag_environment"] = "prod"
57 + reader := collr.MetricStore().Read(metrix.ReadRaw())
58 + requireMetricValue(t, reader, datastoreClusterSpaceUsageCapacityMetric, labels, 1000)
59 + requireMetricValue(t, reader, datastoreClusterSpaceUsageFreeMetric, labels, 400)
60 + requireMetricValue(t, reader, datastoreClusterSpaceUsageUsedMetric, labels, 600)
61 + requireMetricValue(t, reader, datastoreClusterSpaceUtilizationUsedMetric, labels, 6000)
62 + requireMetricValue(t, reader, datastoreClusterStorageDRSEnabledMetric, labels, 1)
63 + requireMetricValue(t, reader, datastoreClusterStorageDRSDisabledMetric, labels, 0)
64 + requireMetricValue(t, reader, datastoreClusterOverallStatusGreenMetric, labels, 1)
65 + requireMetricValue(t, reader, datastoreClusterOverallStatusRedMetric, labels, 0)
66 + requireMetricValue(t, reader, datastoreClusterOverallStatusYellowMetric, labels, 0)
67 + requireMetricValue(t, reader, datastoreClusterOverallStatusGrayMetric, labels, 0)
68 +
69 + createdCharts, createdDims := v2CreatedChartsAndDims(buildV2PlanForTest(t, collr))
70 + chartID := findChartIDByLabelsAndContext(t, createdCharts, "vsphere.datastore_cluster_space_usage", map[string]string{"id": pod.ID})
71 + require.Equal(t, pod.Name, createdCharts[chartID].Labels[datastoreClusterNameLabel])
72 + require.Contains(t, createdDims[chartID], "capacity")
73 + statusChartID := findChartIDByLabelsAndContext(t, createdCharts, "vsphere.datastore_cluster_overall_status", map[string]string{"id": pod.ID})
74 + require.Contains(t, createdDims[statusChartID], "green")
75 + collecttest.AssertChartCoverage(t, collr, collecttest.ChartCoverageExpectation{})
76 + requireChartSelectorsMatchSeries(t, collr, "vsphere.datastore_cluster_")
77 +}
78 +
79 +func TestCollector_DatastoreClustersStorageDRSUnknown(t *testing.T) {
80 + collr := New()
81 + collr.CollectDatastoreClusters = true
82 + pod := testStoragePod("group-p1", "DC0_POD0", 1000, 400, true)
83 + pod.StorageDRSEnabled = nil
84 + collr.resources = &rs.Resources{
85 + StoragePods: rs.StoragePods{pod.ID: pod},
86 + }
87 +
88 + series := runMetricWriteForTest(t, collr, collr.writeDatastoreClusterMetrics)
89 +
90 + requireScalarSeriesValue(t, series, datastoreClusterStorageDRSEnabledMetric, pod.ID, 0)
91 + requireScalarSeriesValue(t, series, datastoreClusterStorageDRSDisabledMetric, pod.ID, 0)
92 +}
93 +
94 +func TestCollector_DatastoreClustersSelector(t *testing.T) {
95 + tests := map[string]struct {
96 + include match.DatastoreClusterIncludes
97 + want int
98 + }{
99 + "selector keeps matching path": {
100 + include: match.DatastoreClusterIncludes{"/DC0/DC0_POD1"},
101 + want: 1,
102 + },
103 + "selector keeps matching name": {
104 + include: match.DatastoreClusterIncludes{"DC0_POD1"},
105 + want: 1,
106 + },
107 + "selector keeps all datastore clusters": {
108 + include: match.DatastoreClusterIncludes{"/*"},
109 + want: 2,
110 + },
111 + "selector can exclude all datastore clusters": {
112 + include: match.DatastoreClusterIncludes{"NoSuchPod"},
113 + want: 0,
114 + },
115 + }
116 +
117 + for name, tc := range tests {
118 + t.Run(name, func(t *testing.T) {
119 + collr, _, teardown := prepareVSphereSim(t)
120 + defer teardown()
121 + collr.CollectDatastoreClusters = true
122 + collr.DatastoreClustersInclude = tc.include
123 +
124 + require.NoError(t, collr.Init(context.Background()))
125 + collr.scraper = mockScraper{collr.scraper}
126 + setOnlyTestStoragePods(collr, matchingTestStoragePods(collr, []*rs.StoragePod{
127 + testStoragePod("group-p1", "DC0_POD0", 1000, 400, true),
128 + testStoragePod("group-p2", "DC0_POD1", 2000, 500, false),
129 + }))
130 +
131 + require.NotEmpty(t, collectScalarSeriesForTest(t, collr))
132 +
133 + require.Equal(t, tc.want, countMetricSeries(collr.MetricStore().Read(metrix.ReadRaw()), datastoreClusterSpaceUsageCapacityMetric))
134 + })
135 + }
136 +}
137 +
138 +func matchingTestStoragePods(collr *Collector, pods []*rs.StoragePod) []*rs.StoragePod {
139 + out := make([]*rs.StoragePod, 0, len(pods))
140 + for _, pod := range pods {
141 + if collr.datastoreClusterMatcher == nil || collr.datastoreClusterMatcher.Match(pod) {
142 + out = append(out, pod)
143 + }
144 + }
145 + return out
146 +}
147 +
148 +func setOnlyTestStoragePods(collr *Collector, pods []*rs.StoragePod) {
149 + collr.resources.StoragePods = make(rs.StoragePods, len(pods))
150 + for _, pod := range pods {
151 + collr.resources.StoragePods.Put(pod)
152 + }
153 +}
154 +
155 +func testStoragePod(id, name string, capacity, freeSpace int64, storageDRSEnabled bool) *rs.StoragePod {
156 + return &rs.StoragePod{
157 + ID: id,
158 + Name: name,
159 + Hier: rs.StoragePodHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC0"}},
160 + Capacity: capacity,
161 + FreeSpace: freeSpace,
162 + StorageDRSEnabled: new(storageDRSEnabled),
163 + OverallStatus: "green",
164 + }
165 +}
166 +
167 +func datastoreClusterLabelsMap(pod *rs.StoragePod) metrix.Labels {
168 + labels := make(metrix.Labels)
169 + labels["id"] = pod.ID
170 + for _, label := range datastoreClusterLabels(pod) {
171 + labels[label.Key] = label.Value
172 + }
173 + return labels
174 +}
175 +
176 +func requireMetricValue(t *testing.T, reader metrix.Reader, name string, labels metrix.Labels, want int64) {
177 + t.Helper()
178 +
179 + got, ok := reader.Value(name, labels)
180 + require.True(t, ok, name)
181 + require.EqualValues(t, want, got)
182 +}
183 +
184 +func findChartIDByLabelsAndContext(t *testing.T, charts map[string]chartengine.CreateChartAction, context string, labels map[string]string) string {
185 + t.Helper()
186 +
187 + for chartID, chart := range charts {
188 + if chart.Meta.Context != context {
189 + continue
190 + }
191 + matches := true
192 + for key, value := range labels {
193 + if chart.Labels[key] != value {
194 + matches = false
195 + break
196 + }
197 + }
198 + if matches {
199 + return chartID
200 + }
201 + }
202 + t.Fatalf("expected %s chart with labels %#v", context, labels)
203 + return ""
204 +}
src/go/plugin/go.d/collector/vsphere/datastore_test.go new
+93
@@ -0,0 +1,93 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +package vsphere
3 +
4 +import (
5 + "testing"
6 +
7 + "github.com/stretchr/testify/require"
8 + "github.com/vmware/govmomi/vim25/types"
9 +
10 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
11 +)
12 +
13 +func TestWriteDatastoreMetrics(t *testing.T) {
14 + yes := true
15 +
16 + tests := map[string]struct {
17 + ds rs.Datastore
18 + want map[string]int64
19 + }{
20 + "accessible datastore": {
21 + ds: rs.Datastore{
22 + ID: "datastore-1",
23 + OverallStatus: "red",
24 + Capacity: 1000,
25 + FreeSpace: 400,
26 + Uncommitted: 250,
27 + Accessible: true,
28 + MaintenanceMode: string(types.DatastoreSummaryMaintenanceModeStateNormal),
29 + MultipleHostAccess: &yes,
30 + },
31 + want: map[string]int64{
32 + "datastore_space_usage_capacity": 1000,
33 + "datastore_space_usage_free": 400,
34 + "datastore_space_usage_used": 600,
35 + "datastore_space_utilization_used": 6000,
36 + "datastore_space_usage_uncommitted": 250,
37 + "datastore_overall_status_green": 0,
38 + "datastore_overall_status_red": 1,
39 + "datastore_overall_status_yellow": 0,
40 + "datastore_overall_status_gray": 0,
41 + "datastore_accessibility_status_accessible": 1,
42 + "datastore_accessibility_status_inaccessible": 0,
43 + "datastore_maintenance_status_normal": 1,
44 + "datastore_maintenance_status_entering_maintenance": 0,
45 + "datastore_maintenance_status_in_maintenance": 0,
46 + "datastore_maintenance_status_unknown": 0,
47 + "datastore_multiple_host_access_enabled": 1,
48 + "datastore_multiple_host_access_disabled": 0,
49 + "datastore_multiple_host_access_unknown": 0,
50 + },
51 + },
52 + "inaccessible datastore": {
53 + ds: rs.Datastore{
54 + ID: "datastore-2",
55 + OverallStatus: "gray",
56 + Capacity: 1000,
57 + FreeSpace: 400,
58 + Uncommitted: 250,
59 + },
60 + want: map[string]int64{
61 + "datastore_space_usage_capacity": 0,
62 + "datastore_space_usage_free": 0,
63 + "datastore_space_usage_used": 0,
64 + "datastore_space_utilization_used": 0,
65 + "datastore_space_usage_uncommitted": 0,
66 + "datastore_overall_status_green": 0,
67 + "datastore_overall_status_red": 0,
68 + "datastore_overall_status_yellow": 0,
69 + "datastore_overall_status_gray": 1,
70 + "datastore_accessibility_status_accessible": 0,
71 + "datastore_accessibility_status_inaccessible": 1,
72 + "datastore_maintenance_status_normal": 0,
73 + "datastore_maintenance_status_entering_maintenance": 0,
74 + "datastore_maintenance_status_in_maintenance": 0,
75 + "datastore_maintenance_status_unknown": 1,
76 + "datastore_multiple_host_access_enabled": 0,
77 + "datastore_multiple_host_access_disabled": 0,
78 + "datastore_multiple_host_access_unknown": 1,
79 + },
80 + },
81 + }
82 +
83 + for name, tc := range tests {
84 + t.Run(name, func(t *testing.T) {
85 + collr := New()
86 + series := runMetricWriteForTest(t, collr, func() { collr.writeDatastoreMetrics(&tc.ds) })
87 + require.Len(t, series, len(tc.want))
88 + for metric, want := range tc.want {
89 + requireScalarSeriesValue(t, series, metric, tc.ds.ID, want)
90 + }
91 + })
92 + }
93 +}
src/go/plugin/go.d/collector/vsphere/discover.go
+6 -5
@@ -2,16 +2,17 @@
2
3 package vsphere
4
5 +import "fmt"
6 +
7 func (c *Collector) goDiscovery() {
6 - if c.discoveryTask != nil {
7 - c.discoveryTask.stop()
8 - }
8 + c.stopDiscoveryTask(false)
9 c.Infof("starting discovery process, will do discovery every %s", c.DiscoveryInterval)
10
11 job := func() {
12 err := c.discoverOnce()
13 if err != nil {
14 - c.Errorf("error on discovering : %v", err)
14 + c.Limit(logKeyDiscoveryError, 1, recurringLogEvery).
15 + Errorf("periodic vSphere discovery failed: %v", err)
16 }
17 }
18 c.discoveryTask = newTask(job, c.DiscoveryInterval.Duration())
@@ -20,7 +21,7 @@ func (c *Collector) goDiscovery() {
21 func (c *Collector) discoverOnce() error {
22 res, err := c.Discover()
23 if err != nil {
23 - return err
24 + return fmt.Errorf("discover vSphere resources through configured discoverer: %w", err)
25 }
26
27 c.collectionLock.Lock()
src/go/plugin/go.d/collector/vsphere/discover/build.go
+241 -72
@@ -8,6 +8,7 @@ import (
8 rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
9
10 "github.com/vmware/govmomi/vim25/mo"
11 + "github.com/vmware/govmomi/vim25/types"
12 )
13
14 func (d Discoverer) build(raw *resources) *rs.Resources {
@@ -22,9 +23,11 @@ func (d Discoverer) build(raw *resources) *rs.Resources {
23 res.Hosts = d.buildHosts(raw.hosts)
24 res.VMs = d.buildVMs(raw.vms)
25 res.Datastores = d.buildDatastores(raw.datastores)
26 + res.Networks = d.buildNetworks(raw.networks)
27 + res.StoragePods = d.buildStoragePods(raw.storagePods)
28 res.ResourcePools = d.buildResourcePools(raw.resourcePools, res.Clusters)
29
27 - d.Infof("discovering : building : built %d/%d dcs, %d/%d folders, %d/%d clusters, %d/%d hosts, %d/%d vms, %d/%d datastores, %d/%d resource pools, process took %s",
30 + d.Infof("discovering : building : built %d/%d dcs, %d/%d folders, %d/%d clusters, %d/%d hosts, %d/%d vms, %d/%d datastores, %d/%d networks, %d/%d datastore clusters, %d/%d resource pools, process took %s",
31 len(res.DataCenters),
32 len(raw.dcs),
33 len(res.Folders),
@@ -37,6 +40,10 @@ func (d Discoverer) build(raw *resources) *rs.Resources {
40 len(raw.vms),
41 len(res.Datastores),
42 len(raw.datastores),
43 + len(res.Networks),
44 + len(raw.networks),
45 + len(res.StoragePods),
46 + len(raw.storagePods),
47 len(res.ResourcePools),
48 len(raw.resourcePools),
49 time.Since(t),
@@ -53,11 +60,7 @@ func fixClustersParentID(res *rs.Resources) {
60 }
61
62 func findClusterDcID(parentID string, folders rs.Folders) string {
56 - f := folders.Get(parentID)
57 - if f == nil {
58 - return parentID
59 - }
60 - return findClusterDcID(f.ParentID, folders)
63 + return findFolderRootID(parentID, folders)
64 }
65
66 func (Discoverer) buildDatacenters(raw []mo.Datacenter) rs.DataCenters {
@@ -79,12 +82,18 @@ func newDC(raw mo.Datacenter) *rs.Datacenter {
82 func (Discoverer) buildFolders(raw []mo.Folder) rs.Folders {
83 fs := make(rs.Folders)
84 for _, d := range raw {
82 - fs.Put(newFolder(d))
85 + if f := newFolder(d); f != nil {
86 + fs.Put(f)
87 + }
88 }
89 return fs
90 }
91
92 func newFolder(raw mo.Folder) *rs.Folder {
93 + parentID := parentRefValue(raw.Parent)
94 + if parentID == "" {
95 + return nil
96 + }
97 // vm group-v55 datacenter-54
98 // host group-h56 datacenter-54
99 // datastore group-s57 datacenter-54
@@ -92,135 +101,268 @@ func newFolder(raw mo.Folder) *rs.Folder {
101 return &rs.Folder{
102 Name: raw.Name,
103 ID: raw.Reference().Value,
95 - ParentID: raw.Parent.Value,
104 + ParentID: parentID,
105 }
106 }
107
108 func (Discoverer) buildClusters(raw []mo.ComputeResource) rs.Clusters {
109 clusters := make(rs.Clusters)
110 for _, c := range raw {
102 - clusters.Put(newCluster(c))
111 + if cluster := newCluster(c); cluster != nil {
112 + clusters.Put(cluster)
113 + }
114 }
115 return clusters
116 }
117
118 func newCluster(raw mo.ComputeResource) *rs.Cluster {
119 + parentID := parentRefValue(raw.Parent)
120 + if parentID == "" {
121 + return nil
122 + }
123 // s - dummy cluster, c - created by user cluster
124 // 192.168.0.201 domain-s61 group-h4
125 // New Cluster1 domain-c52 group-h67
111 - return &rs.Cluster{
112 - Name: raw.Name,
113 - ID: raw.Reference().Value,
114 - ParentID: raw.Parent.Value,
115 - Ref: raw.Reference(),
126 + cluster := &rs.Cluster{
127 + Name: raw.Name,
128 + ID: raw.Reference().Value,
129 + ParentID: parentID,
130 + CustomValues: customFieldValues(raw.CustomValue),
131 + Ref: raw.Reference(),
132 }
133 + rs.SetClusterVSANInfo(cluster, raw.ConfigurationEx)
134 + return cluster
135 }
136
119 -const (
120 - poweredOn = "poweredOn"
121 -)
122 -
137 func (d Discoverer) buildHosts(raw []mo.HostSystem) rs.Hosts {
124 - var num int
138 hosts := make(rs.Hosts)
139 for _, h := range raw {
127 - // poweredOn | poweredOff | standBy | unknown
128 - if h.Runtime.PowerState != poweredOn {
129 - num++
130 - continue
140 + if host := newHost(h); host != nil {
141 + hosts.Put(host)
142 }
132 - // connected | notResponding | disconnected
133 - //if v.Runtime.ConnectionState == "" {
134 - //
135 - //}
136 - hosts.Put(newHost(h))
137 - }
138 - if num > 0 {
139 - d.Infof("discovering : building : removed %d hosts (not powered on)", num)
143 }
144 return hosts
145 }
146
147 func newHost(raw mo.HostSystem) *rs.Host {
148 + parentID := parentRefValue(raw.Parent)
149 + if parentID == "" {
150 + return nil
151 + }
152 // 192.168.0.201 host-22 domain-s61
153 // 192.168.0.202 host-28 domain-c52
154 // 192.168.0.203 host-33 domain-c52
148 - return &rs.Host{
149 - Name: raw.Name,
150 - ID: raw.Reference().Value,
151 - ParentID: raw.Parent.Value,
152 - OverallStatus: string(raw.Summary.OverallStatus),
153 - Ref: raw.Reference(),
155 + host := &rs.Host{
156 + Name: raw.Name,
157 + ID: raw.Reference().Value,
158 + ParentID: parentID,
159 + CustomValues: customFieldValues(raw.CustomValue),
160 + ConnectionState: string(raw.Runtime.ConnectionState),
161 + PowerState: string(raw.Runtime.PowerState),
162 + InMaintenanceMode: raw.Runtime.InMaintenanceMode,
163 + OverallStatus: string(raw.Summary.OverallStatus),
164 + Ref: raw.Reference(),
165 }
166 + if raw.Config != nil && raw.Config.VsanHostConfig != nil && raw.Config.VsanHostConfig.ClusterInfo != nil {
167 + host.VSANNodeUUID = raw.Config.VsanHostConfig.ClusterInfo.NodeUuid
168 + }
169 + return host
170 }
171
172 func (d Discoverer) buildVMs(raw []mo.VirtualMachine) rs.VMs {
158 - var num int
173 vms := make(rs.VMs)
174 for _, v := range raw {
161 - // poweredOff | poweredOn | suspended
162 - if v.Runtime.PowerState != poweredOn {
163 - num++
164 - continue
165 - }
166 - // connected | disconnected | orphaned | inaccessible | invalid
167 - //if v.Runtime.ConnectionState == "" {
168 - //
169 - //}
175 vms.Put(newVM(v))
176 }
172 - if num > 0 {
173 - d.Infof("discovering : building : removed %d vms (not powered on)", num)
174 - }
177 return vms
178 }
179
180 func newVM(raw mo.VirtualMachine) *rs.VM {
181 // deb91 vm-25 group-v3 host-22
182 + var hostID string
183 + if raw.Runtime.Host != nil {
184 + hostID = raw.Runtime.Host.Value
185 + }
186 + var folderID string
187 + if raw.Parent != nil {
188 + folderID = raw.Parent.Value
189 + }
190 + var toolsRunningStatus, toolsVersionStatus string
191 + if raw.Summary.Guest != nil {
192 + toolsRunningStatus = raw.Summary.Guest.ToolsRunningStatus
193 + toolsVersionStatus = raw.Summary.Guest.ToolsVersionStatus2
194 + }
195 + var committed, uncommitted, unshared int64
196 + if raw.Summary.Storage != nil {
197 + committed = raw.Summary.Storage.Committed
198 + uncommitted = raw.Summary.Storage.Uncommitted
199 + unshared = raw.Summary.Storage.Unshared
200 + }
201 + var instanceUUID string
202 + if raw.Config != nil {
203 + instanceUUID = raw.Config.InstanceUuid
204 + }
205 + snapshot := summarizeSnapshotInfo(raw.Snapshot)
206 return &rs.VM{
181 - Name: raw.Name,
182 - ID: raw.Reference().Value,
183 - ParentID: raw.Runtime.Host.Value,
184 - OverallStatus: string(raw.Summary.OverallStatus),
185 - Ref: raw.Reference(),
207 + Name: raw.Name,
208 + ID: raw.Reference().Value,
209 + ParentID: hostID,
210 + FolderParentID: folderID,
211 + CustomValues: customFieldValues(raw.CustomValue),
212 + ConnectionState: string(raw.Runtime.ConnectionState),
213 + PowerState: string(raw.Runtime.PowerState),
214 + ToolsRunningStatus: toolsRunningStatus,
215 + ToolsVersionStatus: toolsVersionStatus,
216 + InstanceUUID: instanceUUID,
217 + ConsolidationNeeded: raw.Runtime.ConsolidationNeeded,
218 + ConfigCPU: int64(raw.Summary.Config.NumCpu),
219 + ConfigMemory: int64(raw.Summary.Config.MemorySizeMB),
220 + ConfigDisks: int64(raw.Summary.Config.NumVirtualDisks),
221 + ConfigNICs: int64(raw.Summary.Config.NumEthernetCards),
222 + StorageCommitted: committed,
223 + StorageUncommitted: uncommitted,
224 + StorageUnshared: unshared,
225 + OverallStatus: string(raw.Summary.OverallStatus),
226 + SnapshotCount: snapshot.count,
227 + SnapshotMaxChainDepth: snapshot.maxChainDepth,
228 + SnapshotOldestCreateTime: snapshot.oldestCreateTime,
229 + Ref: raw.Reference(),
230 }
231 }
232
233 func (d Discoverer) buildDatastores(raw []mo.Datastore) rs.Datastores {
190 - var num int
234 datastores := make(rs.Datastores)
235 for _, ds := range raw {
193 - if !ds.Summary.Accessible {
194 - num++
195 - continue
236 + if datastore := newDatastore(ds); datastore != nil {
237 + datastores.Put(datastore)
238 }
197 - datastores.Put(newDatastore(ds))
198 - }
199 - if num > 0 {
200 - d.Infof("discovering : building : removed %d datastores (not accessible)", num)
239 }
240 return datastores
241 }
242
243 func newDatastore(raw mo.Datastore) *rs.Datastore {
244 + parentID := parentRefValue(raw.Parent)
245 + if parentID == "" {
246 + return nil
247 + }
248 return &rs.Datastore{
249 + Name: raw.Name,
250 + ID: raw.Reference().Value,
251 + ParentID: parentID,
252 + CustomValues: customFieldValues(raw.CustomValue),
253 + OverallStatus: string(raw.OverallStatus),
254 + Type: raw.Summary.Type,
255 + Capacity: raw.Summary.Capacity,
256 + FreeSpace: raw.Summary.FreeSpace,
257 + Uncommitted: raw.Summary.Uncommitted,
258 + Accessible: raw.Summary.Accessible,
259 + MaintenanceMode: raw.Summary.MaintenanceMode,
260 + MultipleHostAccess: raw.Summary.MultipleHostAccess,
261 + Ref: raw.Reference(),
262 + }
263 +}
264 +
265 +func (d Discoverer) buildNetworks(raw []mo.Network) rs.Networks {
266 + networks := make(rs.Networks)
267 + for _, network := range raw {
268 + networks.Put(newNetwork(network))
269 + }
270 + return networks
271 +}
272 +
273 +func newNetwork(raw mo.Network) *rs.Network {
274 + var accessible bool
275 + var ipPoolName string
276 + if raw.Summary != nil {
277 + if summary := raw.Summary.GetNetworkSummary(); summary != nil {
278 + accessible = summary.Accessible
279 + ipPoolName = summary.IpPoolName
280 + }
281 + }
282 +
283 + networkType := raw.Reference().Type
284 + if networkType == "" {
285 + networkType = "Network"
286 + }
287 +
288 + var parentID string
289 + if raw.Parent != nil {
290 + parentID = raw.Parent.Value
291 + }
292 +
293 + return &rs.Network{
294 Name: raw.Name,
295 ID: raw.Reference().Value,
209 - ParentID: raw.Parent.Value,
296 + Type: networkType,
297 + ParentID: parentID,
298 + CustomValues: customFieldValues(raw.CustomValue),
299 + Accessible: accessible,
300 + IPPoolName: ipPoolName,
301 + HostIDs: refValues(raw.Host),
302 + VMIDs: refValues(raw.Vm),
303 OverallStatus: string(raw.OverallStatus),
211 - Type: raw.Summary.Type,
212 - Capacity: raw.Summary.Capacity,
213 - FreeSpace: raw.Summary.FreeSpace,
214 - Accessible: raw.Summary.Accessible,
304 Ref: raw.Reference(),
305 }
306 }
307
308 +func refValues(refs []types.ManagedObjectReference) []string {
309 + if len(refs) == 0 {
310 + return nil
311 + }
312 + values := make([]string, 0, len(refs))
313 + for _, ref := range refs {
314 + if ref.Value != "" {
315 + values = append(values, ref.Value)
316 + }
317 + }
318 + return values
319 +}
320 +
321 +func (d Discoverer) buildStoragePods(raw []mo.StoragePod) rs.StoragePods {
322 + pods := make(rs.StoragePods)
323 + for _, pod := range raw {
324 + if storagePod := newStoragePod(pod); storagePod != nil {
325 + pods.Put(storagePod)
326 + }
327 + }
328 + return pods
329 +}
330 +
331 +func newStoragePod(raw mo.StoragePod) *rs.StoragePod {
332 + parentID := parentRefValue(raw.Parent)
333 + if parentID == "" {
334 + return nil
335 + }
336 + var capacity, freeSpace int64
337 + if raw.Summary != nil {
338 + capacity = raw.Summary.Capacity
339 + freeSpace = raw.Summary.FreeSpace
340 + }
341 + var storageDRSEnabled *bool
342 + if raw.PodStorageDrsEntry != nil {
343 + storageDRSEnabled = new(raw.PodStorageDrsEntry.StorageDrsConfig.PodConfig.Enabled)
344 + }
345 + return &rs.StoragePod{
346 + Name: raw.Name,
347 + ID: raw.Reference().Value,
348 + ParentID: parentID,
349 + CustomValues: customFieldValues(raw.CustomValue),
350 + Capacity: capacity,
351 + FreeSpace: freeSpace,
352 + StorageDRSEnabled: storageDRSEnabled,
353 + OverallStatus: string(raw.OverallStatus),
354 + Ref: raw.Reference(),
355 + }
356 +}
357 +
358 func (d Discoverer) buildResourcePools(raw []mo.ResourcePool, clusters rs.Clusters) rs.ResourcePools {
359 pools := make(rs.ResourcePools)
360 for _, rp := range raw {
361 // owner is the cluster that owns this pool
362 ownerID := rp.Owner.Value
363 + if ownerID == "" {
364 + continue
365 + }
366 // skip pools whose owner is a dummy cluster (standalone host)
367 if isDummyCluster(ownerID) {
368 continue
@@ -236,9 +378,36 @@ func (d Discoverer) buildResourcePools(raw []mo.ResourcePool, clusters rs.Cluste
378
379 func newResourcePool(raw mo.ResourcePool) *rs.ResourcePool {
380 return &rs.ResourcePool{
239 - Name: raw.Name,
240 - ID: raw.Reference().Value,
241 - ParentID: raw.Owner.Value, // owner cluster ref
242 - Ref: raw.Reference(),
381 + Name: raw.Name,
382 + ID: raw.Reference().Value,
383 + ParentID: raw.Owner.Value, // owner cluster ref
384 + CustomValues: customFieldValues(raw.CustomValue),
385 + Ref: raw.Reference(),
386 + }
387 +}
388 +
389 +func customFieldValues(values []types.BaseCustomFieldValue) map[int32]string {
390 + if len(values) == 0 {
391 + return nil
392 + }
393 +
394 + out := make(map[int32]string, len(values))
395 + for _, value := range values {
396 + stringValue, ok := value.(*types.CustomFieldStringValue)
397 + if !ok || stringValue == nil || stringValue.Value == "" {
398 + continue
399 + }
400 + out[stringValue.Key] = stringValue.Value
401 + }
402 + if len(out) == 0 {
403 + return nil
404 + }
405 + return out
406 +}
407 +
408 +func parentRefValue(ref *types.ManagedObjectReference) string {
409 + if ref == nil {
410 + return ""
411 }
412 + return ref.Value
413 }
src/go/plugin/go.d/collector/vsphere/discover/discover.go
+139 -28
@@ -8,6 +8,7 @@ import (
8 "time"
9
10 "github.com/netdata/netdata/go/plugins/logger"
11 + "github.com/netdata/netdata/go/plugins/pkg/matcher"
12 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/match"
13 rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
14
@@ -15,6 +16,13 @@ import (
16 "github.com/vmware/govmomi/vim25/types"
17 )
18
19 +const (
20 + logKeyNetworkTopologyDiscoveryError = "vsphere-discover-network-topology-error"
21 + logKeyDatastoreClusterDiscoveryError = "vsphere-discover-datastore-cluster-error"
22 + logKeyCustomAttributeDiscoveryError = "vsphere-discover-custom-attribute-error"
23 + logKeyTagDiscoveryError = "vsphere-discover-tag-error"
24 +)
25 +
26 type Client interface {
27 Datacenters(pathSet ...string) ([]mo.Datacenter, error)
28 Folders(pathSet ...string) ([]mo.Folder, error)
@@ -22,14 +30,19 @@ type Client interface {
30 Hosts(pathSet ...string) ([]mo.HostSystem, error)
31 VirtualMachines(pathSet ...string) ([]mo.VirtualMachine, error)
32 Datastores(pathSet ...string) ([]mo.Datastore, error)
33 + Networks(pathSet ...string) ([]mo.Network, error)
34 + StoragePods(pathSet ...string) ([]mo.StoragePod, error)
35 ResourcePools(pathSet ...string) ([]mo.ResourcePool, error)
36
37 + CustomFields() ([]types.CustomFieldDef, error)
38 + TagsByRef(refs []types.ManagedObjectReference) (map[types.ManagedObjectReference]map[string][]string, error)
39 CounterInfoByName() (map[string]*types.PerfCounterInfo, error)
40 }
41
42 func New(client Client) *Discoverer {
43 return &Discoverer{
32 - Client: client,
44 + Client: client,
45 + missingPerfCounterWarnings: make(map[string]bool),
46 }
47 }
48
@@ -40,6 +53,13 @@ type Discoverer struct {
53 match.VMMatcher
54 match.DatastoreMatcher
55 match.ClusterMatcher
56 + match.DatastoreClusterMatcher
57 + CollectDatastoreClusters bool
58 + CollectVSAN bool
59 + CollectNetworkTopology bool
60 + TagCategoryMatcher matcher.Matcher
61 + CustomAttributeMatcher matcher.Matcher
62 + missingPerfCounterWarnings map[string]bool
63 }
64
65 type resources struct {
@@ -49,14 +69,16 @@ type resources struct {
69 hosts []mo.HostSystem
70 vms []mo.VirtualMachine
71 datastores []mo.Datastore
72 + networks []mo.Network
73 + storagePods []mo.StoragePod
74 resourcePools []mo.ResourcePool
75 }
76
55 -func (d Discoverer) Discover() (*rs.Resources, error) {
77 +func (d *Discoverer) Discover() (*rs.Resources, error) {
78 startTime := time.Now()
79 raw, err := d.discover()
80 if err != nil {
59 - return nil, fmt.Errorf("discovering resources : %v", err)
81 + return nil, fmt.Errorf("discover vSphere inventory resources: %w", err)
82 }
83
84 res := d.build(raw)
@@ -71,17 +93,20 @@ func (d Discoverer) Discover() (*rs.Resources, error) {
93 numH := len(res.Hosts)
94 numV := len(res.VMs)
95 numD := len(res.Datastores)
74 - removed := d.removeUnmatched(res)
75 - if removed == (numC + numH + numV + numD) {
76 - return nil, fmt.Errorf("all resources were filtered (%d clusters, %d hosts, %d vms, %d datastores)", numC, numH, numV, numD)
96 + numSP := len(res.StoragePods)
97 + d.removeUnmatched(res)
98 + if len(res.Clusters)+len(res.Hosts)+len(res.VMs)+len(res.Datastores)+len(res.StoragePods) == 0 {
99 + return nil, fmt.Errorf("all resources were filtered (%d clusters, %d hosts, %d vms, %d datastores, %d datastore clusters)", numC, numH, numV, numD, numSP)
100 }
101
102 + d.collectEnrichmentLabels(res)
103 +
104 err = d.collectMetricLists(res)
105 if err != nil {
81 - return nil, fmt.Errorf("collecting metric lists : %v", err)
106 + return nil, fmt.Errorf("collect vSphere performance metric lists: %w", err)
107 }
108
84 - d.Infof("discovering : discovered %d/%d clusters, %d/%d hosts, %d/%d vms, %d/%d datastores, %d resource pools, the whole process took %s",
109 + d.Infof("discovering : discovered %d/%d clusters, %d/%d hosts, %d/%d vms, %d/%d datastores, %d/%d datastore clusters, %d resource pools, the whole process took %s",
110 len(res.Clusters),
111 numOfRealClusters(raw.clusters),
112 len(res.Hosts),
@@ -90,6 +115,8 @@ func (d Discoverer) Discover() (*rs.Resources, error) {
115 len(raw.vms),
116 len(res.Datastores),
117 len(raw.datastores),
118 + len(res.StoragePods),
119 + len(raw.storagePods),
120 len(res.ResourcePools),
121 time.Since(startTime))
122
@@ -98,15 +125,65 @@ func (d Discoverer) Discover() (*rs.Resources, error) {
125
126 var (
127 // properties to set
101 - datacenterPathSet = []string{"name", "parent"}
102 - folderPathSet = []string{"name", "parent"}
103 - clusterPathSet = []string{"name", "parent"}
104 - hostPathSet = []string{"name", "parent", "runtime.powerState", "summary.overallStatus"}
105 - vmPathSet = []string{"name", "runtime.host", "runtime.powerState", "summary.overallStatus"}
106 - datastorePathSet = []string{"name", "parent", "summary", "overallStatus"}
107 - resourcePoolPathSet = []string{"name", "owner"}
128 + datacenterPathSet = []string{"name", "parent"}
129 + folderPathSet = []string{"name", "parent"}
130 + clusterPathSetBase = []string{"name", "parent"}
131 + hostPathSetBase = []string{"name", "parent", "runtime.connectionState", "runtime.powerState", "runtime.inMaintenanceMode", "summary.overallStatus"}
132 + vmPathSetBase = []string{"name", "parent", "runtime.host", "runtime.connectionState", "runtime.powerState", "runtime.consolidationNeeded", "summary.guest", "summary.config", "summary.storage", "summary.overallStatus", "snapshot"}
133 + datastorePathSetBase = []string{"name", "parent", "summary", "overallStatus"}
134 + networkPathSetBase = []string{"name", "parent", "summary", "host", "vm", "overallStatus"}
135 + storagePodPathSetBase = []string{"name", "parent", "summary", "overallStatus", "podStorageDrsEntry.storageDrsConfig.podConfig.enabled"}
136 + resourcePoolPathSetBase = []string{"name", "owner"}
137 )
138
139 +func (d Discoverer) clusterPathSet() []string {
140 + pathSet := append([]string(nil), clusterPathSetBase...)
141 + if d.CollectVSAN {
142 + pathSet = append(pathSet, "configurationEx.vsanConfigInfo")
143 + }
144 + return d.withCustomValues(pathSet)
145 +}
146 +
147 +func (d Discoverer) hostPathSet() []string {
148 + pathSet := append([]string(nil), hostPathSetBase...)
149 + if d.CollectVSAN {
150 + pathSet = append(pathSet, "config.vsanHostConfig.clusterInfo.nodeUuid")
151 + }
152 + return d.withCustomValues(pathSet)
153 +}
154 +
155 +func (d Discoverer) vmPathSet() []string {
156 + pathSet := append([]string(nil), vmPathSetBase...)
157 + if d.CollectVSAN {
158 + pathSet = append(pathSet, "config.instanceUuid")
159 + }
160 + return d.withCustomValues(pathSet)
161 +}
162 +
163 +func (d Discoverer) datastorePathSet() []string {
164 + return d.withCustomValues(datastorePathSetBase)
165 +}
166 +
167 +func (d Discoverer) networkPathSet() []string {
168 + return d.withCustomValues(networkPathSetBase)
169 +}
170 +
171 +func (d Discoverer) storagePodPathSet() []string {
172 + return d.withCustomValues(storagePodPathSetBase)
173 +}
174 +
175 +func (d Discoverer) resourcePoolPathSet() []string {
176 + return d.withCustomValues(resourcePoolPathSetBase)
177 +}
178 +
179 +func (d Discoverer) withCustomValues(pathSet []string) []string {
180 + out := append([]string(nil), pathSet...)
181 + if d.CustomAttributeMatcher != nil {
182 + out = append(out, "customValue")
183 + }
184 + return out
185 +}
186 +
187 func (d Discoverer) discover() (*resources, error) {
188 d.Debug("discovering : starting resource discovering process")
189
@@ -114,49 +191,71 @@ func (d Discoverer) discover() (*resources, error) {
191 t := start
192 datacenters, err := d.Datacenters(datacenterPathSet...)
193 if err != nil {
117 - return nil, err
194 + return nil, discoverRetrieveError("datacenters", datacenterPathSet, err)
195 }
196 d.Debugf("discovering : found %d dcs, process took %s", len(datacenters), time.Since(t))
197
198 t = time.Now()
199 folders, err := d.Folders(folderPathSet...)
200 if err != nil {
124 - return nil, err
201 + return nil, discoverRetrieveError("folders", folderPathSet, err)
202 }
203 d.Debugf("discovering : found %d folders, process took %s", len(folders), time.Since(t))
204
205 t = time.Now()
129 - clusters, err := d.ComputeResources(clusterPathSet...)
206 + clusters, err := d.ComputeResources(d.clusterPathSet()...)
207 if err != nil {
131 - return nil, err
208 + return nil, discoverRetrieveError("compute resources", d.clusterPathSet(), err)
209 }
210 d.Debugf("discovering : found %d clusters, process took %s", len(clusters), time.Since(t))
211
212 t = time.Now()
136 - hosts, err := d.Hosts(hostPathSet...)
213 + hosts, err := d.Hosts(d.hostPathSet()...)
214 if err != nil {
138 - return nil, err
215 + return nil, discoverRetrieveError("hosts", d.hostPathSet(), err)
216 }
217 d.Debugf("discovering : found %d hosts, process took %s", len(hosts), time.Since(t))
218
219 t = time.Now()
143 - vms, err := d.VirtualMachines(vmPathSet...)
220 + vms, err := d.VirtualMachines(d.vmPathSet()...)
221 if err != nil {
145 - return nil, err
222 + return nil, discoverRetrieveError("virtual machines", d.vmPathSet(), err)
223 }
224 d.Debugf("discovering : found %d vms, process took %s", len(vms), time.Since(t))
225
226 t = time.Now()
150 - datastores, err := d.Datastores(datastorePathSet...)
227 + datastores, err := d.Datastores(d.datastorePathSet()...)
228 if err != nil {
152 - return nil, err
229 + return nil, discoverRetrieveError("datastores", d.datastorePathSet(), err)
230 }
231 d.Debugf("discovering : found %d datastores, process took %s", len(datastores), time.Since(t))
232
233 + var networks []mo.Network
234 + if d.CollectNetworkTopology {
235 + t = time.Now()
236 + networks, err = d.Networks(d.networkPathSet()...)
237 + if err != nil {
238 + d.warnLimited(logKeyNetworkTopologyDiscoveryError, "discovering : failed to discover networks for topology: %v", discoverRetrieveError("networks", d.networkPathSet(), err))
239 + } else {
240 + d.Debugf("discovering : found %d networks, process took %s", len(networks), time.Since(t))
241 + }
242 + }
243 +
244 + var storagePods []mo.StoragePod
245 + if d.CollectDatastoreClusters {
246 + t = time.Now()
247 + storagePods, err = d.StoragePods(d.storagePodPathSet()...)
248 + if err != nil {
249 + d.warnLimited(logKeyDatastoreClusterDiscoveryError, "discovering : failed to discover datastore clusters: %v", discoverRetrieveError("datastore clusters", d.storagePodPathSet(), err))
250 + } else {
251 + d.Debugf("discovering : found %d datastore clusters, process took %s", len(storagePods), time.Since(t))
252 + }
253 + }
254 +
255 t = time.Now()
157 - resourcePools, err := d.ResourcePools(resourcePoolPathSet...)
256 + resourcePools, err := d.ResourcePools(d.resourcePoolPathSet()...)
257 if err != nil {
159 - return nil, err
258 + return nil, discoverRetrieveError("resource pools", d.resourcePoolPathSet(), err)
259 }
260 d.Debugf("discovering : found %d resource pools, process took %s", len(resourcePools), time.Since(t))
261
@@ -167,10 +266,12 @@ func (d Discoverer) discover() (*resources, error) {
266 hosts: hosts,
267 vms: vms,
268 datastores: datastores,
269 + networks: networks,
270 + storagePods: storagePods,
271 resourcePools: resourcePools,
272 }
273
173 - d.Infof("discovering : found %d dcs, %d folders, %d clusters (%d dummy), %d hosts, %d vms, %d datastores, %d resource pools, process took %s",
274 + d.Infof("discovering : found %d dcs, %d folders, %d clusters (%d dummy), %d hosts, %d vms, %d datastores, %d networks, %d datastore clusters, %d resource pools, process took %s",
275 len(raw.dcs),
276 len(raw.folders),
277 len(clusters),
@@ -178,6 +279,8 @@ func (d Discoverer) discover() (*resources, error) {
279 len(raw.hosts),
280 len(raw.vms),
281 len(raw.datastores),
282 + len(raw.networks),
283 + len(raw.storagePods),
284 len(raw.resourcePools),
285 time.Since(start),
286 )
@@ -185,6 +288,14 @@ func (d Discoverer) discover() (*resources, error) {
288 return &raw, nil
289 }
290
291 +func (d Discoverer) warnLimited(key, format string, args ...any) {
292 + d.Limit(key, 1, time.Hour).Warningf(format, args...)
293 +}
294 +
295 +func discoverRetrieveError(resource string, pathSet []string, err error) error {
296 + return fmt.Errorf("retrieve %s from vSphere inventory pathSet=[%s]: %w", resource, strings.Join(pathSet, ","), err)
297 +}
298 +
299 func numOfDummyClusters(clusters []mo.ComputeResource) (num int) {
300 for _, c := range clusters {
301 // domain-s61 | domain-c52
src/go/plugin/go.d/collector/vsphere/discover/discover_test.go
+461
@@ -4,14 +4,20 @@ package discover
4
5 import (
6 "crypto/tls"
7 + "errors"
8 "net/url"
9 "testing"
10 "time"
11
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"
14 + "github.com/vmware/govmomi/performance"
15 "github.com/vmware/govmomi/simulator"
16 + "github.com/vmware/govmomi/vim25/mo"
17 + "github.com/vmware/govmomi/vim25/types"
18
19 + "github.com/netdata/netdata/go/plugins/logger"
20 + "github.com/netdata/netdata/go/plugins/pkg/matcher"
21 "github.com/netdata/netdata/go/plugins/pkg/tlscfg"
22 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/client"
23 rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
@@ -34,6 +40,80 @@ func TestDiscoverer_Discover(t *testing.T) {
40 assert.True(t, isMetricListsCollected(res))
41 }
42
43 +func TestDiscoverer_DiscoverNetworkTopologyOptIn(t *testing.T) {
44 + d, _, teardown := prepareDiscovererSim(t)
45 + defer teardown()
46 + d.CollectNetworkTopology = true
47 +
48 + res, err := d.Discover()
49 +
50 + require.NoError(t, err)
51 + require.NotEmpty(t, res.Networks)
52 + assert.True(t, isHierarchySet(res))
53 +}
54 +
55 +func TestDiscoverer_networkPathSetIncludesOverallStatus(t *testing.T) {
56 + assert.Contains(t, Discoverer{}.networkPathSet(), "overallStatus")
57 +}
58 +
59 +func TestDiscoverer_DiscoverFailSoftOptionalSurfaces(t *testing.T) {
60 + tests := map[string]struct {
61 + setup func(*Discoverer)
62 + check func(*testing.T, *rs.Resources)
63 + }{
64 + "network topology": {
65 + setup: func(d *Discoverer) {
66 + d.CollectNetworkTopology = true
67 + d.Client = networksErrorClient{Client: d.Client}
68 + },
69 + check: func(t *testing.T, res *rs.Resources) {
70 + require.Empty(t, res.Networks)
71 + assert.NotEmpty(t, res.Datastores)
72 + },
73 + },
74 + "datastore clusters": {
75 + setup: func(d *Discoverer) {
76 + d.CollectDatastoreClusters = true
77 + d.Client = storagePodsErrorClient{Client: d.Client}
78 + },
79 + check: func(t *testing.T, res *rs.Resources) {
80 + require.Empty(t, res.StoragePods)
81 + assert.NotEmpty(t, res.Datastores)
82 + },
83 + },
84 + "custom attributes": {
85 + setup: func(d *Discoverer) {
86 + d.Client = customFieldsErrorClient{Client: d.Client}
87 + d.CustomAttributeMatcher = matcher.TRUE()
88 + },
89 + },
90 + "tags": {
91 + setup: func(d *Discoverer) {
92 + d.Client = tagsByRefErrorClient{Client: d.Client}
93 + d.TagCategoryMatcher = matcher.TRUE()
94 + },
95 + },
96 + }
97 +
98 + for name, tc := range tests {
99 + t.Run(name, func(t *testing.T) {
100 + d, _, teardown := prepareDiscovererSim(t)
101 + defer teardown()
102 + tc.setup(d)
103 +
104 + res, err := d.Discover()
105 +
106 + require.NoError(t, err)
107 + require.NotNil(t, res)
108 + assert.NotEmpty(t, res.Hosts)
109 + assert.NotEmpty(t, res.VMs)
110 + if tc.check != nil {
111 + tc.check(t, res)
112 + }
113 + })
114 + }
115 +}
116 +
117 func TestDiscoverer_discover(t *testing.T) {
118 d, model, teardown := prepareDiscovererSim(t)
119 defer teardown()
@@ -68,6 +148,235 @@ func TestDiscoverer_build(t *testing.T) {
148 assert.Lenf(t, res.Datastores, len(raw.datastores), "datastores")
149 }
150
151 +func TestResourceBuildersSkipMissingParent(t *testing.T) {
152 + tests := map[string]struct {
153 + build func() any
154 + }{
155 + "folder": {build: func() any { return newFolder(mo.Folder{}) }},
156 + "cluster": {build: func() any { return newCluster(mo.ComputeResource{}) }},
157 + "host": {build: func() any { return newHost(mo.HostSystem{}) }},
158 + "datastore": {build: func() any { return newDatastore(mo.Datastore{}) }},
159 + "storage pod": {build: func() any { return newStoragePod(mo.StoragePod{}) }},
160 + }
161 +
162 + for name, tc := range tests {
163 + t.Run(name, func(t *testing.T) {
164 + assert.Nil(t, tc.build())
165 + })
166 + }
167 +}
168 +
169 +func TestDiscoverer_buildHostsKeepsNonPoweredHosts(t *testing.T) {
170 + raw := []mo.HostSystem{
171 + {
172 + ManagedEntity: mo.ManagedEntity{
173 + ExtensibleManagedObject: mo.ExtensibleManagedObject{
174 + Self: types.ManagedObjectReference{Type: "HostSystem", Value: "host-1"},
175 + },
176 + Parent: &types.ManagedObjectReference{Type: "ComputeResource", Value: "domain-c1"},
177 + Name: "host1",
178 + },
179 + Runtime: types.HostRuntimeInfo{PowerState: types.HostSystemPowerStatePoweredOn},
180 + },
181 + {
182 + ManagedEntity: mo.ManagedEntity{
183 + ExtensibleManagedObject: mo.ExtensibleManagedObject{
184 + Self: types.ManagedObjectReference{Type: "HostSystem", Value: "host-2"},
185 + },
186 + Parent: &types.ManagedObjectReference{Type: "ComputeResource", Value: "domain-c1"},
187 + Name: "host2",
188 + },
189 + Runtime: types.HostRuntimeInfo{
190 + ConnectionState: types.HostSystemConnectionStateNotResponding,
191 + PowerState: types.HostSystemPowerStatePoweredOff,
192 + InMaintenanceMode: true,
193 + },
194 + },
195 + }
196 +
197 + hosts := Discoverer{}.buildHosts(raw)
198 + assert.NotNil(t, hosts.Get("host-1"))
199 + assert.NotNil(t, hosts.Get("host-2"))
200 + host := hosts.Get("host-2")
201 + require.NotNil(t, host)
202 + assert.Equal(t, "poweredOff", host.PowerState)
203 + assert.Equal(t, "notResponding", host.ConnectionState)
204 + assert.True(t, host.InMaintenanceMode)
205 +}
206 +
207 +func TestDiscoverer_buildVMsKeepsNonPoweredVMsAndNilHost(t *testing.T) {
208 + raw := []mo.VirtualMachine{
209 + {
210 + ManagedEntity: mo.ManagedEntity{
211 + ExtensibleManagedObject: mo.ExtensibleManagedObject{
212 + Self: types.ManagedObjectReference{Type: "VirtualMachine", Value: "vm-1"},
213 + },
214 + Parent: &types.ManagedObjectReference{Type: "Folder", Value: "group-v1"},
215 + Name: "vm1",
216 + },
217 + Runtime: types.VirtualMachineRuntimeInfo{PowerState: types.VirtualMachinePowerStatePoweredOn},
218 + },
219 + {
220 + ManagedEntity: mo.ManagedEntity{
221 + ExtensibleManagedObject: mo.ExtensibleManagedObject{
222 + Self: types.ManagedObjectReference{Type: "VirtualMachine", Value: "vm-2"},
223 + },
224 + Parent: &types.ManagedObjectReference{Type: "Folder", Value: "group-v1"},
225 + Name: "vm2",
226 + CustomValue: []types.BaseCustomFieldValue{
227 + &types.CustomFieldStringValue{
228 + CustomFieldValue: types.CustomFieldValue{Key: 7},
229 + Value: "owner-a",
230 + },
231 + },
232 + },
233 + Runtime: types.VirtualMachineRuntimeInfo{
234 + ConnectionState: types.VirtualMachineConnectionStateInaccessible,
235 + PowerState: types.VirtualMachinePowerStatePoweredOff,
236 + ConsolidationNeeded: true,
237 + },
238 + Summary: types.VirtualMachineSummary{
239 + Guest: &types.VirtualMachineGuestSummary{
240 + ToolsRunningStatus: string(types.VirtualMachineToolsRunningStatusGuestToolsRunning),
241 + ToolsVersionStatus2: string(types.VirtualMachineToolsVersionStatusGuestToolsTooOld),
242 + },
243 + Config: types.VirtualMachineConfigSummary{
244 + NumCpu: 4,
245 + MemorySizeMB: 8192,
246 + NumVirtualDisks: 2,
247 + NumEthernetCards: 3,
248 + },
249 + Storage: &types.VirtualMachineStorageSummary{
250 + Committed: 100,
251 + Uncommitted: 200,
252 + Unshared: 300,
253 + },
254 + },
255 + },
256 + }
257 + hostRef := types.ManagedObjectReference{Type: "HostSystem", Value: "host-1"}
258 + raw[0].Runtime.Host = &hostRef
259 +
260 + vms := Discoverer{}.buildVMs(raw)
261 + assert.NotNil(t, vms.Get("vm-1"))
262 + vm := vms.Get("vm-2")
263 + require.NotNil(t, vm)
264 + assert.Empty(t, vm.ParentID)
265 + assert.Equal(t, "group-v1", vm.FolderParentID)
266 + assert.Equal(t, "poweredOff", vm.PowerState)
267 + assert.Equal(t, "inaccessible", vm.ConnectionState)
268 + assert.Equal(t, string(types.VirtualMachineToolsRunningStatusGuestToolsRunning), vm.ToolsRunningStatus)
269 + assert.Equal(t, string(types.VirtualMachineToolsVersionStatusGuestToolsTooOld), vm.ToolsVersionStatus)
270 + assert.True(t, vm.ConsolidationNeeded)
271 + assert.EqualValues(t, 4, vm.ConfigCPU)
272 + assert.EqualValues(t, 8192, vm.ConfigMemory)
273 + assert.EqualValues(t, 2, vm.ConfigDisks)
274 + assert.EqualValues(t, 3, vm.ConfigNICs)
275 + assert.EqualValues(t, 100, vm.StorageCommitted)
276 + assert.EqualValues(t, 200, vm.StorageUncommitted)
277 + assert.EqualValues(t, 300, vm.StorageUnshared)
278 + assert.Equal(t, map[int32]string{7: "owner-a"}, vm.CustomValues)
279 +}
280 +
281 +func TestDiscoverer_buildDatastoresKeepsInaccessible(t *testing.T) {
282 + yes := true
283 + raw := []mo.Datastore{
284 + {
285 + ManagedEntity: mo.ManagedEntity{
286 + ExtensibleManagedObject: mo.ExtensibleManagedObject{
287 + Self: types.ManagedObjectReference{Type: "Datastore", Value: "datastore-1"},
288 + },
289 + Parent: &types.ManagedObjectReference{Type: "Folder", Value: "group-s1"},
290 + Name: "Datastore1",
291 + OverallStatus: types.ManagedEntityStatusGray,
292 + },
293 + Summary: types.DatastoreSummary{
294 + Capacity: 1000,
295 + FreeSpace: 400,
296 + Uncommitted: 250,
297 + Accessible: false,
298 + MultipleHostAccess: &yes,
299 + Type: "VMFS",
300 + MaintenanceMode: string(types.DatastoreSummaryMaintenanceModeStateInMaintenance),
301 + },
302 + },
303 + }
304 +
305 + datastores := Discoverer{}.buildDatastores(raw)
306 +
307 + ds := datastores.Get("datastore-1")
308 + require.NotNil(t, ds)
309 + assert.False(t, ds.Accessible)
310 + assert.EqualValues(t, 250, ds.Uncommitted)
311 + assert.Equal(t, string(types.DatastoreSummaryMaintenanceModeStateInMaintenance), ds.MaintenanceMode)
312 + require.NotNil(t, ds.MultipleHostAccess)
313 + assert.True(t, *ds.MultipleHostAccess)
314 +}
315 +
316 +func TestNewNetwork(t *testing.T) {
317 + network := newNetwork(mo.Network{
318 + ManagedEntity: mo.ManagedEntity{
319 + ExtensibleManagedObject: mo.ExtensibleManagedObject{
320 + Self: types.ManagedObjectReference{Type: "Network", Value: "network-1"},
321 + },
322 + Parent: &types.ManagedObjectReference{Type: "Folder", Value: "group-n1"},
323 + OverallStatus: types.ManagedEntityStatusGreen,
324 + },
325 + Name: "VM Network",
326 + Summary: &types.NetworkSummary{
327 + Accessible: true,
328 + IpPoolName: "pool1",
329 + },
330 + Host: []types.ManagedObjectReference{{Type: "HostSystem", Value: "host-1"}},
331 + Vm: []types.ManagedObjectReference{{Type: "VirtualMachine", Value: "vm-1"}},
332 + })
333 +
334 + require.NotNil(t, network)
335 + assert.Equal(t, "network-1", network.ID)
336 + assert.Equal(t, "VM Network", network.Name)
337 + assert.Equal(t, "Network", network.Type)
338 + assert.Equal(t, "group-n1", network.ParentID)
339 + assert.True(t, network.Accessible)
340 + assert.Equal(t, "pool1", network.IPPoolName)
341 + assert.Equal(t, "green", network.OverallStatus)
342 + assert.Equal(t, []string{"host-1"}, network.HostIDs)
343 + assert.Equal(t, []string{"vm-1"}, network.VMIDs)
344 +}
345 +
346 +func TestNewStoragePod(t *testing.T) {
347 + pod := newStoragePod(mo.StoragePod{
348 + Folder: mo.Folder{
349 + ManagedEntity: mo.ManagedEntity{
350 + ExtensibleManagedObject: mo.ExtensibleManagedObject{
351 + Self: types.ManagedObjectReference{Type: "StoragePod", Value: "group-p1"},
352 + },
353 + Parent: &types.ManagedObjectReference{Type: "Folder", Value: "group-s1"},
354 + Name: "DC0_POD0",
355 + OverallStatus: types.ManagedEntityStatusYellow,
356 + },
357 + },
358 + Summary: &types.StoragePodSummary{
359 + Capacity: 1000,
360 + FreeSpace: 400,
361 + },
362 + PodStorageDrsEntry: &types.PodStorageDrsEntry{
363 + StorageDrsConfig: types.StorageDrsConfigInfo{
364 + PodConfig: types.StorageDrsPodConfigInfo{Enabled: true},
365 + },
366 + },
367 + })
368 +
369 + require.NotNil(t, pod)
370 + assert.Equal(t, "group-p1", pod.ID)
371 + assert.Equal(t, "DC0_POD0", pod.Name)
372 + assert.Equal(t, "group-s1", pod.ParentID)
373 + assert.EqualValues(t, 1000, pod.Capacity)
374 + assert.EqualValues(t, 400, pod.FreeSpace)
375 + require.NotNil(t, pod.StorageDRSEnabled)
376 + assert.True(t, *pod.StorageDRSEnabled)
377 + assert.Equal(t, "yellow", pod.OverallStatus)
378 +}
379 +
380 func TestDiscoverer_setHierarchy(t *testing.T) {
381 d, _, teardown := prepareDiscovererSim(t)
382 defer teardown()
@@ -82,6 +391,35 @@ func TestDiscoverer_setHierarchy(t *testing.T) {
391 assert.True(t, isHierarchySet(res))
392 }
393
394 +func TestDiscoverer_setVMHierarchyUsesFolderDatacenterWhenHostMissing(t *testing.T) {
395 + res := &rs.Resources{
396 + DataCenters: rs.DataCenters{
397 + "datacenter-1": &rs.Datacenter{ID: "datacenter-1", Name: "DC1"},
398 + },
399 + Folders: rs.Folders{
400 + "group-v1": &rs.Folder{ID: "group-v1", ParentID: "datacenter-1", Name: "vm"},
401 + },
402 + Hosts: rs.Hosts{},
403 + VMs: rs.VMs{
404 + "vm-1": &rs.VM{ID: "vm-1", FolderParentID: "group-v1"},
405 + },
406 + }
407 +
408 + assert.True(t, setVMHierarchy(res.VMs.Get("vm-1"), res))
409 + assert.Equal(t, "DC1", res.VMs.Get("vm-1").Hier.DC.Name)
410 + assert.Empty(t, res.VMs.Get("vm-1").Hier.Cluster.Name)
411 + assert.Empty(t, res.VMs.Get("vm-1").Hier.Host.Name)
412 +}
413 +
414 +func TestFindFolderRootIDStopsOnCycles(t *testing.T) {
415 + folders := rs.Folders{
416 + "group-v1": &rs.Folder{ID: "group-v1", ParentID: "group-v2", Name: "vm"},
417 + "group-v2": &rs.Folder{ID: "group-v2", ParentID: "group-v1", Name: "nested"},
418 + }
419 +
420 + assert.Empty(t, findVMDcID("group-v1", folders))
421 +}
422 +
423 func TestDiscoverer_removeUnmatched(t *testing.T) {
424 d, _, teardown := prepareDiscovererSim(t)
425 defer teardown()
@@ -102,6 +440,19 @@ func TestDiscoverer_removeUnmatched(t *testing.T) {
440 assert.Lenf(t, res.VMs, 0, "vms")
441 }
442
443 +func TestDiscoverer_removeUnmatchedStoragePods(t *testing.T) {
444 + pods := rs.StoragePods{
445 + "group-p1": &rs.StoragePod{ID: "group-p1"},
446 + "group-p2": &rs.StoragePod{ID: "group-p2"},
447 + }
448 + d := Discoverer{DatastoreClusterMatcher: falseStoragePodMatcher{}}
449 +
450 + removed := d.removeUnmatchedStoragePods(pods)
451 +
452 + require.Equal(t, 2, removed)
453 + require.Empty(t, pods)
454 +}
455 +
456 func TestDiscoverer_collectMetricLists(t *testing.T) {
457 d, _, teardown := prepareDiscovererSim(t)
458 defer teardown()
@@ -116,6 +467,75 @@ func TestDiscoverer_collectMetricLists(t *testing.T) {
467 assert.True(t, isMetricListsCollected(res))
468 }
469
470 +func TestDiscoverer_warnMissingMetricCountersInitializesWarningMap(t *testing.T) {
471 + d := &Discoverer{Logger: logger.New()}
472 +
473 + d.warnMissingMetricCounters(map[string]*types.PerfCounterInfo{})
474 +
475 + require.NotNil(t, d.missingPerfCounterWarnings)
476 + require.NotEmpty(t, d.missingPerfCounterWarnings)
477 +}
478 +
479 +func TestSimpleMetricListIncludesPowerMetrics(t *testing.T) {
480 + tests := map[string]struct {
481 + counters map[string]*types.PerfCounterInfo
482 + build func(map[string]*types.PerfCounterInfo) performance.MetricList
483 + wantLen int
484 + }{
485 + "host": {
486 + counters: map[string]*types.PerfCounterInfo{
487 + "cpu.usage.average": {Key: 1},
488 + "power.power.average": {Key: 2},
489 + "power.powerCap.average": {Key: 3},
490 + "power.energy.summation": {Key: 4},
491 + "power.capacity.usage.average": {Key: 5},
492 + "power.capacity.usagePct.average": {Key: 6},
493 + "power.capacity.usageIdle.average": {Key: 7},
494 + "power.capacity.usageSystem.average": {Key: 8},
495 + "power.capacity.usageVm.average": {Key: 9},
496 + },
497 + build: simpleHostMetricList,
498 + wantLen: 9,
499 + },
500 + "VM": {
501 + counters: map[string]*types.PerfCounterInfo{
502 + "cpu.usage.average": {Key: 1},
503 + "power.power.average": {Key: 2},
504 + "power.energy.summation": {Key: 3},
505 + },
506 + build: simpleVMMetricList,
507 + wantLen: 3,
508 + },
509 + }
510 +
511 + for name, tc := range tests {
512 + t.Run(name, func(t *testing.T) {
513 + ml := tc.build(tc.counters)
514 +
515 + require.Len(t, ml, tc.wantLen)
516 + for _, metric := range ml {
517 + assert.Empty(t, metric.Instance)
518 + }
519 + })
520 + }
521 +}
522 +
523 +func TestExpectedMetricCounterNamesSkipsOptionalCounters(t *testing.T) {
524 + names := expectedMetricCounterNames()
525 +
526 + assert.Contains(t, names, "cpu.usage.average")
527 + for name, counter := range map[string]string{
528 + "host power": "power.power.average",
529 + "host energy": "power.energy.summation",
530 + "cluster DRS score": "clusterServices.clusterDrsScore.latest",
531 + "VM DRS score": "clusterServices.vmDrsScore.latest",
532 + } {
533 + t.Run(name, func(t *testing.T) {
534 + assert.NotContains(t, names, counter)
535 + })
536 + }
537 +}
538 +
539 func prepareDiscovererSim(t *testing.T) (d *Discoverer, model *simulator.Model, teardown func()) {
540 model, srv := createSim(t)
541 teardown = func() { model.Remove(); srv.Close() }
@@ -165,6 +585,11 @@ func isHierarchySet(res *rs.Resources) bool {
585 return false
586 }
587 }
588 + for _, network := range res.Networks {
589 + if !network.Hier.IsSet() {
590 + return false
591 + }
592 + }
593 for _, rp := range res.ResourcePools {
594 if !rp.Hier.IsSet() {
595 return false
@@ -195,3 +620,39 @@ func (falseHostMatcher) Match(*rs.Host) bool { return false }
620 type falseVMMatcher struct{}
621
622 func (falseVMMatcher) Match(*rs.VM) bool { return false }
623 +
624 +type falseStoragePodMatcher struct{}
625 +
626 +func (falseStoragePodMatcher) Match(*rs.StoragePod) bool { return false }
627 +
628 +type storagePodsErrorClient struct {
629 + Client
630 +}
631 +
632 +func (storagePodsErrorClient) StoragePods(...string) ([]mo.StoragePod, error) {
633 + return nil, errors.New("storage pod permission denied")
634 +}
635 +
636 +type networksErrorClient struct {
637 + Client
638 +}
639 +
640 +func (networksErrorClient) Networks(...string) ([]mo.Network, error) {
641 + return nil, errors.New("network permission denied")
642 +}
643 +
644 +type customFieldsErrorClient struct {
645 + Client
646 +}
647 +
648 +func (customFieldsErrorClient) CustomFields() ([]types.CustomFieldDef, error) {
649 + return nil, errors.New("custom field permission denied")
650 +}
651 +
652 +type tagsByRefErrorClient struct {
653 + Client
654 +}
655 +
656 +func (tagsByRefErrorClient) TagsByRef([]types.ManagedObjectReference) (map[types.ManagedObjectReference]map[string][]string, error) {
657 + return nil, errors.New("tag permission denied")
658 +}
src/go/plugin/go.d/collector/vsphere/discover/enrichment_labels.go new
+251
@@ -0,0 +1,251 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package discover
4 +
5 +import (
6 + "fmt"
7 + "sort"
8 + "strings"
9 + "unicode"
10 +
11 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
12 +
13 + "github.com/vmware/govmomi/vim25/types"
14 +)
15 +
16 +const (
17 + tagLabelPrefix = "vsphere_tag_"
18 + customAttributeLabelPrefix = "vsphere_custom_attribute_"
19 +)
20 +
21 +func (d Discoverer) collectEnrichmentLabels(res *rs.Resources) {
22 + if res == nil {
23 + return
24 + }
25 +
26 + if d.CustomAttributeMatcher != nil {
27 + if err := d.collectCustomAttributeLabels(res); err != nil {
28 + d.warnLimited(logKeyCustomAttributeDiscoveryError, "discovering : user metadata labels : collect vSphere custom attribute labels: %v", err)
29 + }
30 + }
31 + if d.TagCategoryMatcher != nil {
32 + if err := d.collectTagLabels(res); err != nil {
33 + d.warnLimited(logKeyTagDiscoveryError, "discovering : user metadata labels : collect vSphere tag labels: %v", err)
34 + }
35 + }
36 +}
37 +
38 +func (d Discoverer) collectCustomAttributeLabels(res *rs.Resources) error {
39 + fields, err := d.CustomFields()
40 + if err != nil {
41 + return fmt.Errorf("collect vSphere custom attribute definitions for user metadata labels: %w", err)
42 + }
43 +
44 + namesByKey := make(map[int32]string)
45 + for _, field := range fields {
46 + if field.Name == "" || !d.CustomAttributeMatcher.Match([]byte(field.Name)) {
47 + continue
48 + }
49 + namesByKey[field.Key] = field.Name
50 + }
51 + if len(namesByKey) == 0 {
52 + return nil
53 + }
54 +
55 + for _, vm := range res.VMs {
56 + addCustomAttributeLabels(&vm.Labels, vm.CustomValues, namesByKey)
57 + }
58 + for _, host := range res.Hosts {
59 + addCustomAttributeLabels(&host.Labels, host.CustomValues, namesByKey)
60 + }
61 + for _, ds := range res.Datastores {
62 + addCustomAttributeLabels(&ds.Labels, ds.CustomValues, namesByKey)
63 + }
64 + for _, cluster := range res.Clusters {
65 + addCustomAttributeLabels(&cluster.Labels, cluster.CustomValues, namesByKey)
66 + }
67 + for _, rp := range res.ResourcePools {
68 + addCustomAttributeLabels(&rp.Labels, rp.CustomValues, namesByKey)
69 + }
70 + for _, sp := range res.StoragePods {
71 + addCustomAttributeLabels(&sp.Labels, sp.CustomValues, namesByKey)
72 + }
73 +
74 + return nil
75 +}
76 +
77 +func addCustomAttributeLabels(labels *map[string]string, values map[int32]string, namesByKey map[int32]string) {
78 + if len(values) == 0 || len(namesByKey) == 0 {
79 + return
80 + }
81 +
82 + type item struct {
83 + name string
84 + value string
85 + }
86 + var items []item
87 + for key, value := range values {
88 + name := namesByKey[key]
89 + if name == "" || value == "" {
90 + continue
91 + }
92 + items = append(items, item{name: name, value: value})
93 + }
94 + sort.Slice(items, func(i, j int) bool { return items[i].name < items[j].name })
95 +
96 + for _, item := range items {
97 + key := metadataLabelKey(customAttributeLabelPrefix, item.name)
98 + addUserMetadataLabel(labels, key, item.value)
99 + }
100 +}
101 +
102 +func (d Discoverer) collectTagLabels(res *rs.Resources) error {
103 + refs := resourceRefs(res)
104 + if len(refs) == 0 {
105 + return nil
106 + }
107 +
108 + tagsByRef, err := d.TagsByRef(refs)
109 + if err != nil {
110 + return fmt.Errorf("collect vSphere tag attachments for %d inventory refs: %w", len(refs), err)
111 + }
112 + for ref, tagsByCategory := range tagsByRef {
113 + addTagLabels(resourceLabelsByRef(res, ref), tagsByCategory, d.TagCategoryMatcher)
114 + }
115 +
116 + return nil
117 +}
118 +
119 +func resourceRefs(res *rs.Resources) []types.ManagedObjectReference {
120 + refs := make([]types.ManagedObjectReference, 0,
121 + len(res.VMs)+len(res.Hosts)+len(res.Datastores)+len(res.Clusters)+len(res.ResourcePools)+len(res.StoragePods))
122 + for _, vm := range res.VMs {
123 + refs = append(refs, vm.Ref)
124 + }
125 + for _, host := range res.Hosts {
126 + refs = append(refs, host.Ref)
127 + }
128 + for _, ds := range res.Datastores {
129 + refs = append(refs, ds.Ref)
130 + }
131 + for _, cluster := range res.Clusters {
132 + refs = append(refs, cluster.Ref)
133 + }
134 + for _, rp := range res.ResourcePools {
135 + refs = append(refs, rp.Ref)
136 + }
137 + for _, sp := range res.StoragePods {
138 + refs = append(refs, sp.Ref)
139 + }
140 + return refs
141 +}
142 +
143 +func resourceLabelsByRef(res *rs.Resources, ref types.ManagedObjectReference) *map[string]string {
144 + if res == nil {
145 + return nil
146 + }
147 + switch ref.Type {
148 + case "VirtualMachine":
149 + if vm := res.VMs.Get(ref.Value); vm != nil {
150 + return &vm.Labels
151 + }
152 + case "HostSystem":
153 + if host := res.Hosts.Get(ref.Value); host != nil {
154 + return &host.Labels
155 + }
156 + case "Datastore":
157 + if ds := res.Datastores.Get(ref.Value); ds != nil {
158 + return &ds.Labels
159 + }
160 + case "ResourcePool":
161 + if rp := res.ResourcePools.Get(ref.Value); rp != nil {
162 + return &rp.Labels
163 + }
164 + case "StoragePod":
165 + if sp := res.StoragePods.Get(ref.Value); sp != nil {
166 + return &sp.Labels
167 + }
168 + }
169 + if cluster := res.Clusters.Get(ref.Value); cluster != nil {
170 + return &cluster.Labels
171 + }
172 + return nil
173 +}
174 +
175 +func addTagLabels(labels *map[string]string, tagsByCategory map[string][]string, categoryMatcher interface{ Match([]byte) bool }) {
176 + if labels == nil || len(tagsByCategory) == 0 || categoryMatcher == nil {
177 + return
178 + }
179 +
180 + categories := make([]string, 0, len(tagsByCategory))
181 + for category := range tagsByCategory {
182 + if category != "" && categoryMatcher.Match([]byte(category)) {
183 + categories = append(categories, category)
184 + }
185 + }
186 + sort.Strings(categories)
187 +
188 + for _, category := range categories {
189 + tags := append([]string(nil), tagsByCategory[category]...)
190 + sort.Strings(tags)
191 + tags = compactNonEmptyStrings(tags)
192 + if len(tags) == 0 {
193 + continue
194 + }
195 + value := strings.Join(tags, "|")
196 + key := metadataLabelKey(tagLabelPrefix, category)
197 + addUserMetadataLabel(labels, key, value)
198 + }
199 +}
200 +
201 +func metadataLabelKey(prefix, name string) string {
202 + suffix := sanitizeLabelKeyPart(name)
203 + if suffix == "" {
204 + return ""
205 + }
206 + return prefix + suffix
207 +}
208 +
209 +func addUserMetadataLabel(labels *map[string]string, key, value string) {
210 + if key == "" || value == "" {
211 + return
212 + }
213 + if *labels == nil {
214 + *labels = make(map[string]string)
215 + }
216 + (*labels)[key] = value
217 +}
218 +
219 +func compactNonEmptyStrings(values []string) []string {
220 + out := values[:0]
221 + var last string
222 + for _, value := range values {
223 + value = strings.TrimSpace(value)
224 + if value == "" || value == last {
225 + continue
226 + }
227 + out = append(out, value)
228 + last = value
229 + }
230 + return out
231 +}
232 +
233 +func sanitizeLabelKeyPart(value string) string {
234 + value = strings.TrimSpace(strings.ToLower(value))
235 + var b strings.Builder
236 + b.Grow(len(value))
237 + lastUnderscore := false
238 + for _, r := range value {
239 + ok := r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
240 + if ok {
241 + b.WriteRune(r)
242 + lastUnderscore = false
243 + continue
244 + }
245 + if !lastUnderscore {
246 + b.WriteByte('_')
247 + lastUnderscore = true
248 + }
249 + }
250 + return strings.Trim(b.String(), "_")
251 +}
src/go/plugin/go.d/collector/vsphere/discover/enrichment_labels_test.go new
+115
@@ -0,0 +1,115 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package discover
4 +
5 +import (
6 + "testing"
7 +
8 + "github.com/stretchr/testify/require"
9 + "github.com/vmware/govmomi/vim25/types"
10 +
11 + "github.com/netdata/netdata/go/plugins/pkg/matcher"
12 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
13 +)
14 +
15 +func TestAddCustomAttributeLabels(t *testing.T) {
16 + var labels map[string]string
17 + values := map[int32]string{
18 + 1: "platform",
19 + 2: "prod",
20 + 3: "secret",
21 + }
22 + namesByKey := map[int32]string{
23 + 1: "Owner Team",
24 + 2: "Env.Name",
25 + }
26 +
27 + addCustomAttributeLabels(&labels, values, namesByKey)
28 +
29 + require.Equal(t, map[string]string{
30 + "vsphere_custom_attribute_env_name": "prod",
31 + "vsphere_custom_attribute_owner_team": "platform",
32 + }, labels)
33 +}
34 +
35 +func TestAddTagLabels(t *testing.T) {
36 + m, err := matcher.NewSimplePatternsMatcher("Business* Env")
37 + require.NoError(t, err)
38 + var labels map[string]string
39 + tagsByCategory := map[string][]string{
40 + "Business Unit": {"Payments", "Core", "Core"},
41 + "Env": {"prod"},
42 + "Secret": {"hidden"},
43 + }
44 +
45 + addTagLabels(&labels, tagsByCategory, m)
46 +
47 + require.Equal(t, map[string]string{
48 + "vsphere_tag_business_unit": "Core|Payments",
49 + "vsphere_tag_env": "prod",
50 + }, labels)
51 +}
52 +
53 +func TestResourceLabelsByRefFindsClusterByValue(t *testing.T) {
54 + res := &rs.Resources{
55 + Clusters: rs.Clusters{
56 + "domain-c1": &rs.Cluster{ID: "domain-c1"},
57 + },
58 + }
59 +
60 + labels := resourceLabelsByRef(res, types.ManagedObjectReference{Type: "ClusterComputeResource", Value: "domain-c1"})
61 + require.NotNil(t, labels)
62 +
63 + addUserMetadataLabel(labels, "vsphere_tag_env", "prod")
64 + require.Equal(t, "prod", res.Clusters.Get("domain-c1").Labels["vsphere_tag_env"])
65 +}
66 +
67 +func TestDiscovererPathSetsAddCustomValueOnlyWhenCustomAttributesEnabled(t *testing.T) {
68 + m, err := matcher.NewSimplePatternsMatcher("Owner")
69 + require.NoError(t, err)
70 +
71 + require.NotContains(t, Discoverer{}.hostPathSet(), "customValue")
72 + for name, pathSet := range map[string][]string{
73 + "hosts": Discoverer{CustomAttributeMatcher: m}.hostPathSet(),
74 + "VMs": Discoverer{CustomAttributeMatcher: m}.vmPathSet(),
75 + "datastores": Discoverer{CustomAttributeMatcher: m}.datastorePathSet(),
76 + "clusters": Discoverer{CustomAttributeMatcher: m}.clusterPathSet(),
77 + "resource pools": Discoverer{CustomAttributeMatcher: m}.resourcePoolPathSet(),
78 + "datastore clusters": Discoverer{CustomAttributeMatcher: m}.storagePodPathSet(),
79 + } {
80 + t.Run(name, func(t *testing.T) {
81 + require.Contains(t, pathSet, "customValue")
82 + })
83 + }
84 +}
85 +
86 +func TestDiscovererPathSetsAddVSANFieldsOnlyWhenEnabled(t *testing.T) {
87 + tests := map[string]struct {
88 + disabled []string
89 + enabled []string
90 + field string
91 + }{
92 + "clusters": {
93 + disabled: Discoverer{}.clusterPathSet(),
94 + enabled: Discoverer{CollectVSAN: true}.clusterPathSet(),
95 + field: "configurationEx.vsanConfigInfo",
96 + },
97 + "hosts": {
98 + disabled: Discoverer{}.hostPathSet(),
99 + enabled: Discoverer{CollectVSAN: true}.hostPathSet(),
100 + field: "config.vsanHostConfig.clusterInfo.nodeUuid",
101 + },
102 + "VMs": {
103 + disabled: Discoverer{}.vmPathSet(),
104 + enabled: Discoverer{CollectVSAN: true}.vmPathSet(),
105 + field: "config.instanceUuid",
106 + },
107 + }
108 +
109 + for name, tc := range tests {
110 + t.Run(name, func(t *testing.T) {
111 + require.NotContains(t, tc.disabled, tc.field)
112 + require.Contains(t, tc.enabled, tc.field)
113 + })
114 + }
115 +}
src/go/plugin/go.d/collector/vsphere/discover/filter.go
+23 -2
@@ -25,13 +25,14 @@ func (d Discoverer) matchVM(vm *rs.VM) bool {
25 func (d Discoverer) removeUnmatched(res *rs.Resources) (removed int) {
26 d.Debug("discovering : filtering : starting filtering resources process")
27 t := time.Now()
28 - numC, numH, numV, numD := len(res.Clusters), len(res.Hosts), len(res.VMs), len(res.Datastores)
28 + numC, numH, numV, numD, numSP := len(res.Clusters), len(res.Hosts), len(res.VMs), len(res.Datastores), len(res.StoragePods)
29 removed += d.removeUnmatchedClusters(res.Clusters)
30 d.removeOrphanedResourcePools(res.ResourcePools, res.Clusters)
31 removed += d.removeUnmatchedHosts(res.Hosts)
32 removed += d.removeUnmatchedVMs(res.VMs)
33 removed += d.removeUnmatchedDatastores(res.Datastores)
34 - d.Infof("discovering : filtering : filtered %d/%d clusters, %d/%d hosts, %d/%d vms, %d/%d datastores, %d resource pools remaining, process took %s",
34 + removed += d.removeUnmatchedStoragePods(res.StoragePods)
35 + d.Infof("discovering : filtering : filtered %d/%d clusters, %d/%d hosts, %d/%d vms, %d/%d datastores, %d/%d datastore clusters, %d resource pools remaining, process took %s",
36 numC-len(res.Clusters),
37 numC,
38 numH-len(res.Hosts),
@@ -40,6 +41,8 @@ func (d Discoverer) removeUnmatched(res *rs.Resources) (removed int) {
41 numV,
42 numD-len(res.Datastores),
43 numD,
44 + numSP-len(res.StoragePods),
45 + numSP,
46 len(res.ResourcePools),
47 time.Since(t))
48 return
@@ -85,6 +88,24 @@ func (d Discoverer) removeUnmatchedDatastores(datastores rs.Datastores) (removed
88 return removed
89 }
90
91 +func (d Discoverer) matchStoragePod(pod *rs.StoragePod) bool {
92 + if d.DatastoreClusterMatcher == nil {
93 + return true
94 + }
95 + return d.DatastoreClusterMatcher.Match(pod)
96 +}
97 +
98 +func (d Discoverer) removeUnmatchedStoragePods(pods rs.StoragePods) (removed int) {
99 + for _, pod := range pods {
100 + if !d.matchStoragePod(pod) {
101 + removed++
102 + pods.Remove(pod.ID)
103 + }
104 + }
105 + d.Debugf("discovering : filtering : removed %d unmatched datastore clusters", removed)
106 + return removed
107 +}
108 +
109 func (d Discoverer) matchCluster(cluster *rs.Cluster) bool {
110 // dummy clusters (standalone host placeholders) are always excluded
111 if isDummyCluster(cluster.ID) {
src/go/plugin/go.d/collector/vsphere/discover/hierarchy.go
+89 -5
@@ -8,6 +8,8 @@ import (
8 rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
9 )
10
11 +const maxHierarchyParentDepth = 64
12 +
13 func (d Discoverer) setHierarchy(res *rs.Resources) error {
14 d.Debug("discovering : hierarchy : start setting resources hierarchy process")
15 t := time.Now()
@@ -16,13 +18,17 @@ func (d Discoverer) setHierarchy(res *rs.Resources) error {
18 h := d.setHostsHierarchy(res)
19 v := d.setVMsHierarchy(res)
20 ds := d.setDatastoresHierarchy(res)
21 + nw := d.setNetworksHierarchy(res)
22 + sp := d.setStoragePodsHierarchy(res)
23 rp := d.setResourcePoolsHierarchy(res)
24
21 - d.Infof("discovering : hierarchy : set %d/%d clusters, %d/%d hosts, %d/%d vms, %d/%d datastores, %d/%d resource pools, process took %s",
25 + d.Infof("discovering : hierarchy : set %d/%d clusters, %d/%d hosts, %d/%d vms, %d/%d datastores, %d/%d networks, %d/%d datastore clusters, %d/%d resource pools, process took %s",
26 c, len(res.Clusters),
27 h, len(res.Hosts),
28 v, len(res.VMs),
29 ds, len(res.Datastores),
30 + nw, len(res.Networks),
31 + sp, len(res.StoragePods),
32 rp, len(res.ResourcePools),
33 time.Since(t),
34 )
@@ -82,6 +88,13 @@ func setHostHierarchy(host *rs.Host, res *rs.Resources) bool {
88 }
89
90 func setVMHierarchy(vm *rs.VM, res *rs.Resources) bool {
91 + if setVMHostHierarchy(vm, res) {
92 + return true
93 + }
94 + return setVMFolderHierarchy(vm, res)
95 +}
96 +
97 +func setVMHostHierarchy(vm *rs.VM, res *rs.Resources) bool {
98 h := res.Hosts.Get(vm.ParentID)
99 if h == nil {
100 return false
@@ -102,6 +115,20 @@ func setVMHierarchy(vm *rs.VM, res *rs.Resources) bool {
115 return vm.Hier.IsSet()
116 }
117
118 +func setVMFolderHierarchy(vm *rs.VM, res *rs.Resources) bool {
119 + dcID := findVMDcID(vm.FolderParentID, res.Folders)
120 + dc := res.DataCenters.Get(dcID)
121 + if dc == nil {
122 + return false
123 + }
124 + vm.Hier.DC.Set(dc.ID, dc.Name)
125 + return vm.Hier.DC.IsSet()
126 +}
127 +
128 +func findVMDcID(parentID string, folders rs.Folders) string {
129 + return findFolderRootID(parentID, folders)
130 +}
131 +
132 func (d Discoverer) setDatastoresHierarchy(res *rs.Resources) (set int) {
133 for _, ds := range res.Datastores {
134 if setDatastoreHierarchy(ds, res) {
@@ -123,11 +150,68 @@ func setDatastoreHierarchy(ds *rs.Datastore, res *rs.Resources) bool {
150 }
151
152 func findDatastoreDcID(parentID string, folders rs.Folders) string {
126 - f := folders.Get(parentID)
127 - if f == nil {
128 - return parentID
153 + return findFolderRootID(parentID, folders)
154 +}
155 +
156 +func (d Discoverer) setNetworksHierarchy(res *rs.Resources) (set int) {
157 + for _, network := range res.Networks {
158 + if setNetworkHierarchy(network, res) {
159 + set++
160 + }
161 + }
162 + return set
163 +}
164 +
165 +// Network parent is normally a network folder (group-n*) which resolves to a datacenter.
166 +func setNetworkHierarchy(network *rs.Network, res *rs.Resources) bool {
167 + dcID := findNetworkDcID(network.ParentID, res.Folders)
168 + dc := res.DataCenters.Get(dcID)
169 + if dc == nil {
170 + return false
171 + }
172 + network.Hier.DC.Set(dc.ID, dc.Name)
173 + return network.Hier.IsSet()
174 +}
175 +
176 +func findNetworkDcID(parentID string, folders rs.Folders) string {
177 + return findFolderRootID(parentID, folders)
178 +}
179 +
180 +func findFolderRootID(parentID string, folders rs.Folders) string {
181 + seen := make(map[string]struct{})
182 + for depth := 0; parentID != "" && depth < maxHierarchyParentDepth; depth++ {
183 + if _, ok := seen[parentID]; ok {
184 + return ""
185 + }
186 + seen[parentID] = struct{}{}
187 +
188 + f := folders.Get(parentID)
189 + if f == nil {
190 + return parentID
191 + }
192 + parentID = f.ParentID
193 + }
194 + return parentID
195 +}
196 +
197 +func (d Discoverer) setStoragePodsHierarchy(res *rs.Resources) (set int) {
198 + for _, pod := range res.StoragePods {
199 + if setStoragePodHierarchy(pod, res) {
200 + set++
201 + }
202 + }
203 + return set
204 +}
205 +
206 +// StoragePod parent is a folder (group-s*) which resolves to a datacenter.
207 +func setStoragePodHierarchy(pod *rs.StoragePod, res *rs.Resources) bool {
208 + dcID := findDatastoreDcID(pod.ParentID, res.Folders)
209 + dc := res.DataCenters.Get(dcID)
210 + if dc == nil {
211 + return false
212 }
130 - return findDatastoreDcID(f.ParentID, folders)
213 + pod.Hier.DC.Set(dc.ID, dc.Name)
214 + return pod.Hier.IsSet()
215 }
216
217 func (d Discoverer) setResourcePoolsHierarchy(res *rs.Resources) (set int) {
src/go/plugin/go.d/collector/vsphere/discover/metric_lists.go
+73 -20
@@ -3,6 +3,7 @@
3 package discover
4
5 import (
6 + "fmt"
7 "sort"
8 "time"
9
@@ -12,13 +13,14 @@ import (
13 "github.com/vmware/govmomi/vim25/types"
14 )
15
15 -func (d Discoverer) collectMetricLists(res *rs.Resources) error {
16 +func (d *Discoverer) collectMetricLists(res *rs.Resources) error {
17 d.Debug("discovering : metric lists : starting resources metric lists collection process")
18 t := time.Now()
19 perfCounters, err := d.CounterInfoByName()
20 if err != nil {
20 - return err
21 + return fmt.Errorf("load vSphere performance counter registry for metric-list mapping: %w", err)
22 }
23 + d.warnMissingMetricCounters(perfCounters)
24
25 hostML := simpleHostMetricList(perfCounters)
26 for _, h := range res.Hosts {
@@ -48,44 +50,76 @@ func (d Discoverer) collectMetricLists(res *rs.Resources) error {
50 return nil
51 }
52
53 +func (d *Discoverer) warnMissingMetricCounters(pci map[string]*types.PerfCounterInfo) {
54 + names := expectedMetricCounterNames()
55 + for _, name := range names {
56 + if _, ok := pci[name]; ok {
57 + continue
58 + }
59 + if d.missingPerfCounterWarnings == nil {
60 + d.missingPerfCounterWarnings = make(map[string]bool)
61 + }
62 + if d.missingPerfCounterWarnings[name] {
63 + continue
64 + }
65 + d.missingPerfCounterWarnings[name] = true
66 + d.Warningf("discovering : metric lists : performance counter %q not found in vCenter registry; metrics using it will be skipped", name)
67 + }
68 +}
69 +
70 +func expectedMetricCounterNames() []string {
71 + var names []string
72 + names = append(names, hostMetrics...)
73 + names = append(names, vmMetrics...)
74 + names = append(names, datastoreMetrics...)
75 + names = append(names, clusterMetrics...)
76 + return uniqueSortedStrings(names)
77 +}
78 +
79 +func uniqueSortedStrings(in []string) []string {
80 + sort.Strings(in)
81 + out := in[:0]
82 + for _, name := range in {
83 + if len(out) == 0 || out[len(out)-1] != name {
84 + out = append(out, name)
85 + }
86 + }
87 + return out
88 +}
89 +
90 func simpleHostMetricList(pci map[string]*types.PerfCounterInfo) performance.MetricList {
52 - return simpleMetricList(hostMetrics, pci)
91 + ml := simpleMetricList(hostMetrics, pci, "")
92 + ml = append(ml, simpleMetricList(hostPowerMetrics, pci, "")...)
93 + return ml
94 }
95
96 func simpleVMMetricList(pci map[string]*types.PerfCounterInfo) performance.MetricList {
56 - return simpleMetricList(vmMetrics, pci)
97 + ml := simpleMetricList(vmMetrics, pci, "")
98 + ml = append(ml, simpleMetricList(vmPowerMetrics, pci, "")...)
99 + return ml
100 }
101
102 func simpleDatastoreMetricList(pci map[string]*types.PerfCounterInfo) performance.MetricList {
60 - return simpleMetricList(datastoreMetrics, pci)
103 + return simpleMetricList(datastoreMetrics, pci, "")
104 }
105
106 func simpleClusterMetricList(pci map[string]*types.PerfCounterInfo) performance.MetricList {
64 - return simpleMetricList(clusterMetrics, pci)
107 + ml := simpleMetricList(clusterMetrics, pci, "")
108 + ml = append(ml, simpleMetricList(clusterOptionalMetrics, pci, "")...)
109 + return ml
110 }
111
67 -func simpleMetricList(metrics []string, pci map[string]*types.PerfCounterInfo) performance.MetricList {
112 +func simpleMetricList(metrics []string, pci map[string]*types.PerfCounterInfo, instance string) performance.MetricList {
113 + metrics = append([]string(nil), metrics...)
114 sort.Strings(metrics)
115
116 var pml performance.MetricList
117 for _, v := range metrics {
118 m, ok := pci[v]
119 if !ok {
74 - // TODO: should be logged
120 continue
121 }
77 - // TODO: only summary metrics for now
78 - // TODO: some metrics only appear if Instance is *, for example
79 - // virtualDisk.totalWriteLatency.average.scsi0:0
80 - // virtualDisk.numberWriteAveraged.average.scsi0:0
81 - // virtualDisk.write.average.scsi0:0
82 - // virtualDisk.totalReadLatency.average.scsi0:0
83 - // virtualDisk.numberReadAveraged.average.scsi0:0
84 - // virtualDisk.read.average.scsi0:0
85 - // disk.numberReadAveraged.average
86 - // disk.numberWriteAveraged.average
87 - // TODO: metrics will be unsorted after if at least one Instance is *
88 - pml = append(pml, types.PerfMetricId{CounterId: m.Key, Instance: ""})
122 + pml = append(pml, types.PerfMetricId{CounterId: m.Key, Instance: instance})
123 }
124 return pml
125 }
@@ -161,6 +195,23 @@ var (
195 "sys.uptime.latest",
196 }
197
198 + hostPowerMetrics = []string{
199 + "power.capacity.usable.average",
200 + "power.capacity.usage.average",
201 + "power.capacity.usageIdle.average",
202 + "power.capacity.usagePct.average",
203 + "power.capacity.usageSystem.average",
204 + "power.capacity.usageVm.average",
205 + "power.energy.summation",
206 + "power.power.average",
207 + "power.powerCap.average",
208 + }
209 +
210 + vmPowerMetrics = []string{
211 + "power.energy.summation",
212 + "power.power.average",
213 + }
214 +
215 // All Level 1 (available at default vCenter settings), IntervalId=300 (historical)
216 clusterMetrics = []string{
217 // clusterServices counters
@@ -203,7 +254,9 @@ var (
254 "vmop.numRebootGuest.latest",
255 "vmop.numShutdownGuest.latest",
256 "vmop.numStandbyGuest.latest",
257 + }
258
259 + clusterOptionalMetrics = []string{
260 // vSphere 7.0+ only — automatically skipped if not available
261 "clusterServices.clusterDrsScore.latest",
262 "clusterServices.vmDrsScore.latest",
src/go/plugin/go.d/collector/vsphere/discover/snapshot.go new
+47
@@ -0,0 +1,47 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package discover
4 +
5 +import (
6 + "time"
7 +
8 + "github.com/vmware/govmomi/vim25/types"
9 +)
10 +
11 +const maxSnapshotTreeDepth = 64
12 +
13 +type snapshotSummary struct {
14 + count int64
15 + maxChainDepth int64
16 + oldestCreateTime time.Time
17 +}
18 +
19 +func summarizeSnapshotInfo(info *types.VirtualMachineSnapshotInfo) snapshotSummary {
20 + if info == nil {
21 + return snapshotSummary{}
22 + }
23 +
24 + var summary snapshotSummary
25 + for i := range info.RootSnapshotList {
26 + walkSnapshotTree(info.RootSnapshotList[i], 1, &summary)
27 + }
28 + return summary
29 +}
30 +
31 +func walkSnapshotTree(node types.VirtualMachineSnapshotTree, depth int64, summary *snapshotSummary) {
32 + if depth > maxSnapshotTreeDepth {
33 + return
34 + }
35 +
36 + summary.count++
37 + if depth > summary.maxChainDepth {
38 + summary.maxChainDepth = depth
39 + }
40 + if !node.CreateTime.IsZero() && (summary.oldestCreateTime.IsZero() || node.CreateTime.Before(summary.oldestCreateTime)) {
41 + summary.oldestCreateTime = node.CreateTime
42 + }
43 +
44 + for i := range node.ChildSnapshotList {
45 + walkSnapshotTree(node.ChildSnapshotList[i], depth+1, summary)
46 + }
47 +}
src/go/plugin/go.d/collector/vsphere/discover/snapshot_test.go new
+96
@@ -0,0 +1,96 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package discover
4 +
5 +import (
6 + "testing"
7 + "time"
8 +
9 + "github.com/stretchr/testify/assert"
10 + "github.com/vmware/govmomi/vim25/types"
11 +)
12 +
13 +func TestSummarizeSnapshotInfo(t *testing.T) {
14 + now := time.Now().UTC()
15 + older := now.Add(-48 * time.Hour)
16 + newer := now.Add(-2 * time.Hour)
17 + oldest := now.Add(-72 * time.Hour)
18 +
19 + tests := map[string]struct {
20 + info *types.VirtualMachineSnapshotInfo
21 + want snapshotSummary
22 + }{
23 + "nil snapshot info": {},
24 + "empty root list": {
25 + info: &types.VirtualMachineSnapshotInfo{},
26 + },
27 + "single snapshot": {
28 + info: &types.VirtualMachineSnapshotInfo{
29 + RootSnapshotList: []types.VirtualMachineSnapshotTree{
30 + {CreateTime: older},
31 + },
32 + },
33 + want: snapshotSummary{
34 + count: 1,
35 + maxChainDepth: 1,
36 + oldestCreateTime: older,
37 + },
38 + },
39 + "siblings and nested chain": {
40 + info: &types.VirtualMachineSnapshotInfo{
41 + RootSnapshotList: []types.VirtualMachineSnapshotTree{
42 + {
43 + CreateTime: newer,
44 + ChildSnapshotList: []types.VirtualMachineSnapshotTree{
45 + {
46 + CreateTime: older,
47 + ChildSnapshotList: []types.VirtualMachineSnapshotTree{
48 + {CreateTime: oldest},
49 + },
50 + },
51 + },
52 + },
53 + {CreateTime: now},
54 + },
55 + },
56 + want: snapshotSummary{
57 + count: 4,
58 + maxChainDepth: 3,
59 + oldestCreateTime: oldest,
60 + },
61 + },
62 + "zero create time still counts": {
63 + info: &types.VirtualMachineSnapshotInfo{
64 + RootSnapshotList: []types.VirtualMachineSnapshotTree{
65 + {},
66 + },
67 + },
68 + want: snapshotSummary{
69 + count: 1,
70 + maxChainDepth: 1,
71 + },
72 + },
73 + }
74 +
75 + for name, tc := range tests {
76 + t.Run(name, func(t *testing.T) {
77 + assert.Equal(t, tc.want, summarizeSnapshotInfo(tc.info))
78 + })
79 + }
80 +}
81 +
82 +func TestSummarizeSnapshotInfoCapsTraversalDepth(t *testing.T) {
83 + root := types.VirtualMachineSnapshotTree{}
84 + node := &root
85 + for i := int64(1); i < maxSnapshotTreeDepth+10; i++ {
86 + node.ChildSnapshotList = []types.VirtualMachineSnapshotTree{{}}
87 + node = &node.ChildSnapshotList[0]
88 + }
89 +
90 + summary := summarizeSnapshotInfo(&types.VirtualMachineSnapshotInfo{
91 + RootSnapshotList: []types.VirtualMachineSnapshotTree{root},
92 + })
93 +
94 + assert.EqualValues(t, maxSnapshotTreeDepth, summary.count)
95 + assert.EqualValues(t, maxSnapshotTreeDepth, summary.maxChainDepth)
96 +}
src/go/plugin/go.d/collector/vsphere/func_readiness.go new
+438
@@ -0,0 +1,438 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import (
6 + "context"
7 + "fmt"
8 + "sort"
9 +
10 + "github.com/netdata/netdata/go/plugins/pkg/funcapi"
11 + "github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi"
12 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
13 + scrapepkg "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/scrape"
14 +)
15 +
16 +const (
17 + readinessMethodID = "readiness"
18 + readinessMethodHelp = "Reports vSphere collector readiness, cached discovery state, and optional capability gates."
19 +)
20 +
21 +type readinessStatus string
22 +
23 +const (
24 + readinessStatusOK readinessStatus = "ok"
25 + readinessStatusWarning readinessStatus = "warning"
26 + readinessStatusDisabled readinessStatus = "disabled"
27 + readinessStatusNotReady readinessStatus = "not_ready"
28 +)
29 +
30 +type readinessRow struct {
31 + check string
32 + scope string
33 + status readinessStatus
34 + details string
35 +}
36 +
37 +type readinessColumn struct {
38 + funcapi.ColumnMeta
39 + value func(readinessRow) any
40 +}
41 +
42 +var readinessColumns = []readinessColumn{
43 + {
44 + ColumnMeta: funcapi.ColumnMeta{
45 + Name: "check",
46 + Tooltip: "Check",
47 + Type: funcapi.FieldTypeString,
48 + Visible: true,
49 + Sortable: true,
50 + Sticky: true,
51 + Summary: funcapi.FieldSummaryCount,
52 + Filter: funcapi.FieldFilterMultiselect,
53 + Transform: funcapi.FieldTransformText,
54 + Visualization: funcapi.FieldVisualValue,
55 + UniqueKey: true,
56 + },
57 + value: func(row readinessRow) any { return row.check },
58 + },
59 + {
60 + ColumnMeta: funcapi.ColumnMeta{
61 + Name: "scope",
62 + Tooltip: "Scope",
63 + Type: funcapi.FieldTypeString,
64 + Visible: true,
65 + Sortable: true,
66 + Summary: funcapi.FieldSummaryCount,
67 + Filter: funcapi.FieldFilterMultiselect,
68 + Transform: funcapi.FieldTransformText,
69 + Visualization: funcapi.FieldVisualValue,
70 + },
71 + value: func(row readinessRow) any { return row.scope },
72 + },
73 + {
74 + ColumnMeta: funcapi.ColumnMeta{
75 + Name: "status",
76 + Tooltip: "Status",
77 + Type: funcapi.FieldTypeString,
78 + Visible: true,
79 + Sortable: true,
80 + Summary: funcapi.FieldSummaryCount,
81 + Filter: funcapi.FieldFilterMultiselect,
82 + Transform: funcapi.FieldTransformText,
83 + Visualization: funcapi.FieldVisualPill,
84 + },
85 + value: func(row readinessRow) any { return string(row.status) },
86 + },
87 + {
88 + ColumnMeta: funcapi.ColumnMeta{
89 + Name: "details",
90 + Tooltip: "Details",
91 + Type: funcapi.FieldTypeString,
92 + Visible: true,
93 + Sortable: false,
94 + Summary: funcapi.FieldSummaryCount,
95 + Filter: funcapi.FieldFilterNone,
96 + Transform: funcapi.FieldTransformText,
97 + Visualization: funcapi.FieldVisualValue,
98 + FullWidth: true,
99 + Wrap: true,
100 + },
101 + value: func(row readinessRow) any { return row.details },
102 + },
103 +}
104 +
105 +type funcReadiness struct {
106 + collector *Collector
107 +}
108 +
109 +var _ funcapi.MethodHandler = (*funcReadiness)(nil)
110 +
111 +func vsphereMethods() []funcapi.MethodConfig {
112 + return []funcapi.MethodConfig{{
113 + ID: readinessMethodID,
114 + Name: "vSphere Readiness",
115 + UpdateEvery: 30,
116 + Help: readinessMethodHelp,
117 + RequireCloud: true,
118 + }, vsphereTopologyMethodConfig()}
119 +}
120 +
121 +func vsphereMethodHandler(job collectorapi.RuntimeJob) funcapi.MethodHandler {
122 + c, ok := job.Collector().(*Collector)
123 + if !ok {
124 + return nil
125 + }
126 + return &funcVSphere{
127 + readiness: &funcReadiness{collector: c},
128 + topology: &funcTopology{collector: c, agentID: job.FullName()},
129 + }
130 +}
131 +
132 +type funcVSphere struct {
133 + readiness *funcReadiness
134 + topology *funcTopology
135 +}
136 +
137 +var _ funcapi.MethodHandler = (*funcVSphere)(nil)
138 +
139 +func (f *funcVSphere) MethodParams(ctx context.Context, method string) ([]funcapi.ParamConfig, error) {
140 + switch method {
141 + case readinessMethodID:
142 + return f.readiness.MethodParams(ctx, method)
143 + case topologyMethodID:
144 + return f.topology.MethodParams(ctx, method)
145 + default:
146 + return nil, nil
147 + }
148 +}
149 +
150 +func (f *funcVSphere) Handle(ctx context.Context, method string, params funcapi.ResolvedParams) *funcapi.FunctionResponse {
151 + switch method {
152 + case readinessMethodID:
153 + return f.readiness.Handle(ctx, method, params)
154 + case topologyMethodID:
155 + return f.topology.Handle(ctx, method, params)
156 + default:
157 + return funcapi.NotFoundResponse(method)
158 + }
159 +}
160 +
161 +func (f *funcVSphere) Cleanup(ctx context.Context) {
162 + f.readiness.Cleanup(ctx)
163 + f.topology.Cleanup(ctx)
164 +}
165 +
166 +func (f *funcReadiness) MethodParams(context.Context, string) ([]funcapi.ParamConfig, error) {
167 + return nil, nil
168 +}
169 +
170 +func (f *funcReadiness) Handle(_ context.Context, method string, _ funcapi.ResolvedParams) *funcapi.FunctionResponse {
171 + if method != readinessMethodID {
172 + return funcapi.NotFoundResponse(method)
173 + }
174 + if f.collector == nil {
175 + return funcapi.UnavailableResponse("collector is not initialized")
176 + }
177 +
178 + rows := f.collector.readinessRows()
179 + sort.SliceStable(rows, func(i, j int) bool {
180 + if rows[i].scope != rows[j].scope {
181 + return rows[i].scope < rows[j].scope
182 + }
183 + return rows[i].check < rows[j].check
184 + })
185 +
186 + cs := funcapi.Columns(readinessColumns, func(col readinessColumn) funcapi.ColumnMeta {
187 + return col.ColumnMeta
188 + })
189 + data := make([][]any, 0, len(rows))
190 + for _, row := range rows {
191 + values := make([]any, len(readinessColumns))
192 + for i, col := range readinessColumns {
193 + values[i] = col.value(row)
194 + }
195 + data = append(data, values)
196 + }
197 +
198 + return &funcapi.FunctionResponse{
199 + Status: 200,
200 + Help: readinessMethodHelp,
201 + Columns: cs.BuildColumns(),
202 + Data: data,
203 + DefaultSortColumn: "status",
204 + }
205 +}
206 +
207 +func (f *funcReadiness) Cleanup(context.Context) {
208 + // No per-invocation resources are allocated by the readiness function.
209 +}
210 +
211 +func (c *Collector) readinessRows() []readinessRow {
212 + c.collectionLock.RLock()
213 + defer c.collectionLock.RUnlock()
214 +
215 + rows := []readinessRow{
216 + configReadinessRow("target_url", "target", c.URL != "", "target URL is configured", "target URL is not configured"),
217 + configReadinessRow("credentials", "target", c.Username != "" && c.Password != "", "credentials are configured", "username or password is not configured"),
218 + configReadinessRow("client", "target", c.vsClient != nil && c.discoverer != nil && c.scraper != nil, "vSphere client, discoverer, and scraper are initialized", "collector has not completed initialization"),
219 + }
220 +
221 + if c.resources == nil {
222 + rows = append(rows,
223 + readinessRow{
224 + check: "inventory_cache",
225 + scope: "discovery",
226 + status: readinessStatusNotReady,
227 + details: "initial discovery has not completed successfully yet",
228 + },
229 + readinessRow{
230 + check: "performance_counters",
231 + scope: "discovery",
232 + status: readinessStatusNotReady,
233 + details: "performance counter lists are unavailable until discovery succeeds",
234 + },
235 + )
236 + } else {
237 + rows = append(rows,
238 + readinessRow{
239 + check: "inventory_cache",
240 + scope: "discovery",
241 + status: readinessStatusOK,
242 + details: resourceCounts(c.resources),
243 + },
244 + c.performanceCountersReadinessRow(),
245 + )
246 + }
247 +
248 + rows = append(rows,
249 + c.userMetadataReadinessRow(),
250 + c.optionalMetricReadinessRow("datastore_clusters", "metrics", c.CollectDatastoreClusters, len(c.DatastoreClustersInclude)),
251 + c.booleanReadinessRow("network_topology", "topology", c.CollectNetworkTopology, "vSphere Network topology discovery is enabled", "vSphere Network topology discovery is disabled"),
252 + c.vsanReadinessRow(),
253 + )
254 +
255 + return rows
256 +}
257 +
258 +func configReadinessRow(check, scope string, ok bool, okDetails, notOKDetails string) readinessRow {
259 + if ok {
260 + return readinessRow{check: check, scope: scope, status: readinessStatusOK, details: okDetails}
261 + }
262 + return readinessRow{check: check, scope: scope, status: readinessStatusNotReady, details: notOKDetails}
263 +}
264 +
265 +func (c *Collector) booleanReadinessRow(check, scope string, enabled bool, enabledDetails, disabledDetails string) readinessRow {
266 + if enabled {
267 + return readinessRow{check: check, scope: scope, status: readinessStatusOK, details: enabledDetails}
268 + }
269 + return readinessRow{check: check, scope: scope, status: readinessStatusDisabled, details: disabledDetails}
270 +}
271 +
272 +func (c *Collector) optionalMetricReadinessRow(check, scope string, enabled bool, includePatterns int) readinessRow {
273 + if !enabled {
274 + return readinessRow{
275 + check: check,
276 + scope: scope,
277 + status: readinessStatusDisabled,
278 + details: "optional collection is disabled",
279 + }
280 + }
281 + return readinessRow{
282 + check: check,
283 + scope: scope,
284 + status: readinessStatusOK,
285 + details: fmt.Sprintf("enabled with include_patterns=%d", includePatterns),
286 + }
287 +}
288 +
289 +func (c *Collector) userMetadataReadinessRow() readinessRow {
290 + tags := len(c.TagCategories)
291 + attrs := len(c.CustomAttributes)
292 + if tags == 0 && attrs == 0 {
293 + return readinessRow{
294 + check: "user_metadata_labels",
295 + scope: "labels",
296 + status: readinessStatusDisabled,
297 + details: "vSphere tag and custom-attribute labels are disabled",
298 + }
299 + }
300 + return readinessRow{
301 + check: "user_metadata_labels",
302 + scope: "labels",
303 + status: readinessStatusOK,
304 + details: fmt.Sprintf("enabled for %d tag category pattern(s), %d custom attribute pattern(s)", tags, attrs),
305 + }
306 +}
307 +
308 +func (c *Collector) vsanReadinessRow() readinessRow {
309 + if !c.CollectVSAN {
310 + return readinessRow{
311 + check: "vsan",
312 + scope: "metrics",
313 + status: readinessStatusDisabled,
314 + details: "vSAN collection is disabled",
315 + }
316 + }
317 + if c.resources == nil {
318 + return readinessRow{
319 + check: "vsan",
320 + scope: "metrics",
321 + status: readinessStatusNotReady,
322 + details: "vSAN collection is enabled, but discovery has not completed",
323 + }
324 + }
325 +
326 + clusters, hosts, vms := c.vsanResources()
327 + if len(clusters) == 0 {
328 + return readinessRow{
329 + check: "vsan",
330 + scope: "metrics",
331 + status: readinessStatusWarning,
332 + details: "vSAN collection is enabled, but no vSAN-enabled clusters match the vSAN selectors",
333 + }
334 + }
335 + if c.vsanMetrics == nil {
336 + return readinessRow{
337 + check: "vsan",
338 + scope: "metrics",
339 + status: readinessStatusNotReady,
340 + details: fmt.Sprintf("vSAN collection is enabled for %d cluster(s), %d host(s), and %d VM(s) after selectors, but no vSAN scrape data is cached yet", len(clusters), len(hosts), len(vms)),
341 + }
342 + }
343 + if vsanMetricsEmpty(c.vsanMetrics) {
344 + return readinessRow{
345 + check: "vsan",
346 + scope: "metrics",
347 + status: readinessStatusWarning,
348 + details: fmt.Sprintf("vSAN collection is enabled for %d cluster(s), %d host(s), and %d VM(s) after selectors, but the last vSAN scrape returned no data", len(clusters), len(hosts), len(vms)),
349 + }
350 + }
351 +
352 + return readinessRow{
353 + check: "vsan",
354 + scope: "metrics",
355 + status: readinessStatusOK,
356 + details: fmt.Sprintf(
357 + "vSAN data cached for %d cluster metric group(s), %d host metric group(s), %d VM metric group(s), %d space result(s), and %d health result(s)",
358 + len(c.vsanMetrics.Clusters),
359 + len(c.vsanMetrics.Hosts),
360 + len(c.vsanMetrics.VMs),
361 + len(c.vsanMetrics.Space),
362 + len(c.vsanMetrics.Health),
363 + ),
364 + }
365 +}
366 +
367 +func vsanMetricsEmpty(metrics *scrapepkg.VSANMetrics) bool {
368 + return metrics == nil ||
369 + len(metrics.Clusters)+len(metrics.Hosts)+len(metrics.VMs)+len(metrics.Space)+len(metrics.Health) == 0
370 +}
371 +
372 +func (c *Collector) performanceCountersReadinessRow() readinessRow {
373 + var hosts, hostsWithMetrics, vms, vmsWithMetrics, datastores, datastoresWithMetrics, clusters, clustersWithMetrics int
374 + for _, host := range c.resources.Hosts {
375 + hosts++
376 + if len(host.MetricList) > 0 {
377 + hostsWithMetrics++
378 + }
379 + }
380 + for _, vm := range c.resources.VMs {
381 + vms++
382 + if len(vm.MetricList) > 0 {
383 + vmsWithMetrics++
384 + }
385 + }
386 + for _, datastore := range c.resources.Datastores {
387 + datastores++
388 + if len(datastore.MetricList) > 0 {
389 + datastoresWithMetrics++
390 + }
391 + }
392 + for _, cluster := range c.resources.Clusters {
393 + clusters++
394 + if len(cluster.MetricList) > 0 {
395 + clustersWithMetrics++
396 + }
397 + }
398 +
399 + status := readinessStatusOK
400 + if (hosts > 0 && hostsWithMetrics == 0) ||
401 + (vms > 0 && vmsWithMetrics == 0) ||
402 + (datastores > 0 && datastoresWithMetrics == 0) ||
403 + (clusters > 0 && clustersWithMetrics == 0) {
404 + status = readinessStatusWarning
405 + }
406 +
407 + return readinessRow{
408 + check: "performance_counters",
409 + scope: "discovery",
410 + status: status,
411 + details: fmt.Sprintf(
412 + "metric lists: %d/%d hosts, %d/%d VMs, %d/%d datastores, %d/%d clusters",
413 + hostsWithMetrics,
414 + hosts,
415 + vmsWithMetrics,
416 + vms,
417 + datastoresWithMetrics,
418 + datastores,
419 + clustersWithMetrics,
420 + clusters,
421 + ),
422 + }
423 +}
424 +
425 +func resourceCounts(resources *rs.Resources) string {
426 + return fmt.Sprintf(
427 + "discovered %d datacenter(s), %d folder(s), %d cluster(s), %d host(s), %d VM(s), %d datastore(s), %d network(s), %d datastore cluster(s), and %d resource pool(s)",
428 + len(resources.DataCenters),
429 + len(resources.Folders),
430 + len(resources.Clusters),
431 + len(resources.Hosts),
432 + len(resources.VMs),
433 + len(resources.Datastores),
434 + len(resources.Networks),
435 + len(resources.StoragePods),
436 + len(resources.ResourcePools),
437 + )
438 +}
src/go/plugin/go.d/collector/vsphere/func_readiness_test.go new
+216
@@ -0,0 +1,216 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import (
6 + "context"
7 + "testing"
8 +
9 + "github.com/stretchr/testify/require"
10 + "github.com/vmware/govmomi/performance"
11 +
12 + "github.com/netdata/netdata/go/plugins/pkg/funcapi"
13 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
14 + scrapepkg "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/scrape"
15 +)
16 +
17 +func TestVSphereMethods(t *testing.T) {
18 + methods := vsphereMethods()
19 +
20 + require.Len(t, methods, 2)
21 + byID := make(map[string]funcapi.MethodConfig, len(methods))
22 + for _, method := range methods {
23 + byID[method.ID] = method
24 + }
25 +
26 + tests := map[string]struct {
27 + wantName string
28 + responseType string
29 + alias string
30 + presentation bool
31 + }{
32 + "readiness": {
33 + wantName: "vSphere Readiness",
34 + },
35 + "topology:vsphere": {
36 + wantName: "vSphere Topology",
37 + responseType: "topology",
38 + alias: "topology:vsphere",
39 + presentation: true,
40 + },
41 + }
42 +
43 + for id, tc := range tests {
44 + t.Run(id, func(t *testing.T) {
45 + method := byID[id]
46 + require.Equal(t, tc.wantName, method.Name)
47 + require.Equal(t, 30, method.UpdateEvery)
48 + require.True(t, method.RequireCloud)
49 + require.False(t, method.AgentWide)
50 + if tc.alias != "" {
51 + require.Contains(t, method.Aliases, tc.alias)
52 + }
53 + if tc.responseType != "" {
54 + require.Equal(t, tc.responseType, method.ResponseType)
55 + }
56 + if tc.presentation {
57 + require.NotNil(t, method.Presentation())
58 + }
59 + })
60 + }
61 +}
62 +
63 +func TestFuncReadiness_Handle(t *testing.T) {
64 + tests := map[string]struct {
65 + method string
66 + collector func() *Collector
67 + want int
68 + check func(*testing.T, map[string][]any)
69 + }{
70 + "without discovery": {
71 + method: "readiness",
72 + collector: New,
73 + want: 200,
74 + check: func(t *testing.T, rows map[string][]any) {
75 + require.Equal(t, "not_ready", rows["target_url"][2])
76 + require.Equal(t, "not_ready", rows["inventory_cache"][2])
77 + require.Equal(t, "disabled", rows["vsan"][2])
78 + },
79 + },
80 + "partial init state": {
81 + method: "readiness",
82 + collector: func() *Collector {
83 + collr := New()
84 + collr.URL = "https://vcenter.local"
85 + collr.Username = "user"
86 + collr.Password = "[REDACTED_SECRET]"
87 + return collr
88 + },
89 + want: 200,
90 + check: func(t *testing.T, rows map[string][]any) {
91 + require.Equal(t, "ok", rows["target_url"][2])
92 + require.Equal(t, "ok", rows["credentials"][2])
93 + require.Equal(t, "not_ready", rows["client"][2])
94 + require.Equal(t, "not_ready", rows["inventory_cache"][2])
95 + },
96 + },
97 + "client row requires vSphere client": {
98 + method: "readiness",
99 + collector: func() *Collector {
100 + collr := New()
101 + collr.URL = "https://vcenter.local"
102 + collr.Username = "user"
103 + collr.Password = "[REDACTED_SECRET]"
104 + collr.discoverer = readinessDiscoverer{}
105 + collr.scraper = readinessScraper{}
106 + return collr
107 + },
108 + want: 200,
109 + check: func(t *testing.T, rows map[string][]any) {
110 + require.Equal(t, "not_ready", rows["client"][2])
111 + },
112 + },
113 + "with vSAN cached data": {
114 + method: "readiness",
115 + collector: func() *Collector {
116 + collr := newVSANTestCollector(true)
117 + collr.URL = "https://vcenter.local"
118 + collr.Username = "user"
119 + collr.Password = "[REDACTED_SECRET]"
120 + for _, cluster := range collr.resources.Clusters {
121 + cluster.MetricList = performance.MetricList{{CounterId: 1}}
122 + }
123 + for _, host := range collr.resources.Hosts {
124 + host.MetricList = performance.MetricList{{CounterId: 1}}
125 + }
126 + for _, vm := range collr.resources.VMs {
127 + vm.MetricList = performance.MetricList{{CounterId: 1}}
128 + }
129 + return collr
130 + },
131 + want: 200,
132 + check: func(t *testing.T, rows map[string][]any) {
133 + require.Equal(t, "ok", rows["target_url"][2])
134 + require.Equal(t, "ok", rows["credentials"][2])
135 + require.Equal(t, "ok", rows["inventory_cache"][2])
136 + require.Equal(t, "ok", rows["vsan"][2])
137 + require.Contains(t, rows["vsan"][3], "vSAN data cached")
138 + },
139 + },
140 + "with empty vSAN cached data": {
141 + method: "readiness",
142 + collector: func() *Collector {
143 + collr := newVSANTestCollector(true)
144 + collr.vsanMetrics = &scrapepkg.VSANMetrics{}
145 + return collr
146 + },
147 + want: 200,
148 + check: func(t *testing.T, rows map[string][]any) {
149 + require.Equal(t, "warning", rows["vsan"][2])
150 + require.Contains(t, rows["vsan"][3], "last vSAN scrape returned no data")
151 + },
152 + },
153 + "unknown method": {
154 + method: "unknown",
155 + collector: New,
156 + want: 404,
157 + },
158 + }
159 +
160 + for name, tc := range tests {
161 + t.Run(name, func(t *testing.T) {
162 + handler := &funcReadiness{collector: tc.collector()}
163 +
164 + resp := handler.Handle(context.Background(), tc.method, nil)
165 +
166 + require.Equal(t, tc.want, resp.Status)
167 + if tc.check != nil {
168 + require.NotEmpty(t, resp.Columns)
169 + tc.check(t, readinessRowsFromResponse(t, resp))
170 + }
171 + })
172 + }
173 +}
174 +
175 +func readinessRowsFromResponse(t *testing.T, resp *funcapi.FunctionResponse) map[string][]any {
176 + t.Helper()
177 +
178 + rows, ok := resp.Data.([][]any)
179 + require.True(t, ok)
180 + byCheck := make(map[string][]any, len(rows))
181 + for _, row := range rows {
182 + require.Len(t, row, len(readinessColumns))
183 + check, ok := row[0].(string)
184 + require.True(t, ok)
185 + byCheck[check] = row
186 + }
187 + return byCheck
188 +}
189 +
190 +type readinessDiscoverer struct{}
191 +
192 +func (readinessDiscoverer) Discover() (*rs.Resources, error) {
193 + return nil, nil
194 +}
195 +
196 +type readinessScraper struct{}
197 +
198 +func (readinessScraper) ScrapeHosts(rs.Hosts) []performance.EntityMetric {
199 + return nil
200 +}
201 +
202 +func (readinessScraper) ScrapeVMs(rs.VMs) []performance.EntityMetric {
203 + return nil
204 +}
205 +
206 +func (readinessScraper) ScrapeDatastores(rs.Datastores) []performance.EntityMetric {
207 + return nil
208 +}
209 +
210 +func (readinessScraper) ScrapeClusters(rs.Clusters) []performance.EntityMetric {
211 + return nil
212 +}
213 +
214 +func (readinessScraper) ScrapeVSAN(rs.Clusters, rs.Hosts, rs.VMs) *scrapepkg.VSANMetrics {
215 + return nil
216 +}
src/go/plugin/go.d/collector/vsphere/func_topology.go new
+374
@@ -0,0 +1,374 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import (
6 + "context"
7 + "sort"
8 + "strings"
9 + "time"
10 +
11 + "github.com/netdata/netdata/go/plugins/pkg/funcapi"
12 + "github.com/netdata/netdata/go/plugins/pkg/topology"
13 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
14 +)
15 +
16 +const (
17 + topologyMethodID = "topology:vsphere"
18 + topologyMethodHelp = "Reports cached vSphere inventory topology for datacenters, clusters, hosts, VMs, datastores, networks, datastore clusters, and resource pools."
19 +
20 + vsphereTopologySchemaVersion = "2.0"
21 + vsphereTopologySource = "vsphere"
22 + vsphereTopologyLayer = "virtualization"
23 + vsphereTopologyView = "inventory"
24 +)
25 +
26 +type funcTopology struct {
27 + collector *Collector
28 + agentID string
29 +}
30 +
31 +var _ funcapi.MethodHandler = (*funcTopology)(nil)
32 +
33 +func vsphereTopologyMethodConfig() funcapi.MethodConfig {
34 + return funcapi.MethodConfig{
35 + ID: topologyMethodID,
36 + Aliases: []string{topologyMethodID},
37 + Name: "vSphere Topology",
38 + UpdateEvery: 30,
39 + Help: topologyMethodHelp,
40 + RequireCloud: true,
41 + ResponseType: "topology",
42 + }.WithPresentation(vsphereTopologyPresentation())
43 +}
44 +
45 +func (f *funcTopology) MethodParams(context.Context, string) ([]funcapi.ParamConfig, error) {
46 + return nil, nil
47 +}
48 +
49 +func (f *funcTopology) Handle(_ context.Context, method string, _ funcapi.ResolvedParams) *funcapi.FunctionResponse {
50 + if method != topologyMethodID {
51 + return funcapi.NotFoundResponse(method)
52 + }
53 + if f.collector == nil {
54 + return funcapi.UnavailableResponse("collector is not initialized")
55 + }
56 +
57 + data, ok := f.collector.topologyData(f.agentID)
58 + if !ok {
59 + return funcapi.UnavailableResponse("topology data not available yet, please retry after discovery")
60 + }
61 +
62 + return &funcapi.FunctionResponse{
63 + Status: 200,
64 + Help: topologyMethodHelp,
65 + ResponseType: "topology",
66 + Data: data,
67 + }
68 +}
69 +
70 +func (f *funcTopology) Cleanup(context.Context) {
71 + // No per-invocation resources are allocated by the topology function.
72 +}
73 +
74 +func (c *Collector) topologyData(agentID string) (topology.Data, bool) {
75 + c.collectionLock.RLock()
76 + defer c.collectionLock.RUnlock()
77 +
78 + if c.resources == nil {
79 + return topology.Data{}, false
80 + }
81 +
82 + actors := make([]topology.Actor, 0, topologyActorCount(c.resources))
83 + links := make([]topology.Link, 0, topologyLinkCount(c.resources))
84 +
85 + for _, dc := range sortedDatacenters(c.resources.DataCenters) {
86 + actors = append(actors, vsphereTopologyActor("vsphere_datacenter", dc.ID, dc.Name, nil, nil))
87 + }
88 + for _, cluster := range sortedClusters(c.resources.Clusters) {
89 + actors = append(actors, vsphereTopologyActor("vsphere_cluster", cluster.ID, cluster.Name, map[string]any{
90 + "overall_status": cluster.OverallStatus,
91 + "drs_enabled": cluster.DrsEnabled,
92 + "ha_enabled": cluster.HaEnabled,
93 + "vsan_enabled": cluster.VSANEnabled,
94 + }, map[string]string{
95 + "datacenter": cluster.Hier.DC.Name,
96 + }))
97 + if c.resources.DataCenters.Get(cluster.Hier.DC.ID) != nil {
98 + links = append(links, vsphereTopologyLink(cluster.Hier.DC.ID, cluster.ID, "contains", "Datacenter contains cluster"))
99 + }
100 + }
101 + for _, host := range sortedHosts(c.resources.Hosts) {
102 + actors = append(actors, vsphereTopologyActor("vsphere_host", host.ID, host.Name, map[string]any{
103 + "connection_state": host.ConnectionState,
104 + "power_state": host.PowerState,
105 + "in_maintenance_mode": host.InMaintenanceMode,
106 + "overall_status": host.OverallStatus,
107 + }, map[string]string{
108 + "datacenter": host.Hier.DC.Name,
109 + "cluster": host.Hier.Cluster.Name,
110 + }))
111 + if c.resources.Clusters.Get(host.Hier.Cluster.ID) != nil {
112 + links = append(links, vsphereTopologyLink(host.Hier.Cluster.ID, host.ID, "contains", "Cluster contains ESXi host"))
113 + } else if c.resources.DataCenters.Get(host.Hier.DC.ID) != nil {
114 + links = append(links, vsphereTopologyLink(host.Hier.DC.ID, host.ID, "contains", "Datacenter contains ESXi host"))
115 + }
116 + }
117 + for _, vm := range sortedVMs(c.resources.VMs) {
118 + actors = append(actors, vsphereTopologyActor("vsphere_vm", vm.ID, vm.Name, map[string]any{
119 + "connection_state": vm.ConnectionState,
120 + "power_state": vm.PowerState,
121 + "overall_status": vm.OverallStatus,
122 + "tools_running_status": vm.ToolsRunningStatus,
123 + "tools_version_status": vm.ToolsVersionStatus,
124 + "consolidation_needed": vm.ConsolidationNeeded,
125 + "snapshot_count": vm.SnapshotCount,
126 + "snapshot_chain_depth": vm.SnapshotMaxChainDepth,
127 + "configured_vcpus": vm.ConfigCPU,
128 + "configured_memory_mib": vm.ConfigMemory,
129 + }, map[string]string{
130 + "datacenter": vm.Hier.DC.Name,
131 + "cluster": vm.Hier.Cluster.Name,
132 + "host": vm.Hier.Host.Name,
133 + }))
134 + switch {
135 + case c.resources.Hosts.Get(vm.Hier.Host.ID) != nil:
136 + links = append(links, vsphereTopologyLink(vm.Hier.Host.ID, vm.ID, "runs", "ESXi host runs VM"))
137 + case c.resources.Clusters.Get(vm.Hier.Cluster.ID) != nil:
138 + links = append(links, vsphereTopologyLink(vm.Hier.Cluster.ID, vm.ID, "contains", "Cluster contains VM"))
139 + case c.resources.DataCenters.Get(vm.Hier.DC.ID) != nil:
140 + links = append(links, vsphereTopologyLink(vm.Hier.DC.ID, vm.ID, "contains", "Datacenter contains VM"))
141 + }
142 + }
143 + for _, datastore := range sortedDatastores(c.resources.Datastores) {
144 + attrs := map[string]any{
145 + "type": datastore.Type,
146 + "overall_status": datastore.OverallStatus,
147 + "accessible": datastore.Accessible,
148 + "maintenance_mode": datastore.MaintenanceMode,
149 + "capacity_bytes": datastore.Capacity,
150 + "free_space_bytes": datastore.FreeSpace,
151 + "uncommitted_bytes": datastore.Uncommitted,
152 + "multiple_host_access": datastore.MultipleHostAccess,
153 + }
154 + actors = append(actors, vsphereTopologyActor("vsphere_datastore", datastore.ID, datastore.Name, attrs, map[string]string{
155 + "datacenter": datastore.Hier.DC.Name,
156 + "type": datastore.Type,
157 + }))
158 + if c.resources.DataCenters.Get(datastore.Hier.DC.ID) != nil {
159 + links = append(links, vsphereTopologyLink(datastore.Hier.DC.ID, datastore.ID, "contains", "Datacenter contains datastore"))
160 + }
161 + }
162 + for _, network := range sortedNetworks(c.resources.Networks) {
163 + actors = append(actors, vsphereTopologyActor("vsphere_network", network.ID, network.Name, map[string]any{
164 + "type": network.Type,
165 + "accessible": network.Accessible,
166 + "ip_pool_name": network.IPPoolName,
167 + "overall_status": network.OverallStatus,
168 + "hosts": len(network.HostIDs),
169 + "vms": len(network.VMIDs),
170 + }, map[string]string{
171 + "datacenter": network.Hier.DC.Name,
172 + "type": network.Type,
173 + }))
174 + if c.resources.DataCenters.Get(network.Hier.DC.ID) != nil {
175 + links = append(links, vsphereTopologyLink(network.Hier.DC.ID, network.ID, "contains", "Datacenter contains network"))
176 + }
177 + for _, hostID := range network.HostIDs {
178 + if c.resources.Hosts.Get(hostID) != nil {
179 + links = append(links, vsphereTopologyLink(hostID, network.ID, "connects", "ESXi host connects to network"))
180 + }
181 + }
182 + for _, vmID := range network.VMIDs {
183 + if c.resources.VMs.Get(vmID) != nil {
184 + links = append(links, vsphereTopologyLink(vmID, network.ID, "connects", "VM connects to network"))
185 + }
186 + }
187 + }
188 + for _, pod := range sortedStoragePods(c.resources.StoragePods) {
189 + actors = append(actors, vsphereTopologyActor("vsphere_datastore_cluster", pod.ID, pod.Name, map[string]any{
190 + "capacity_bytes": pod.Capacity,
191 + "free_space_bytes": pod.FreeSpace,
192 + "storage_drs_enabled": optionalBool(pod.StorageDRSEnabled),
193 + }, map[string]string{
194 + "datacenter": pod.Hier.DC.Name,
195 + }))
196 + if c.resources.DataCenters.Get(pod.Hier.DC.ID) != nil {
197 + links = append(links, vsphereTopologyLink(pod.Hier.DC.ID, pod.ID, "contains", "Datacenter contains datastore cluster"))
198 + }
199 + }
200 + for _, pool := range sortedResourcePools(c.resources.ResourcePools) {
201 + actors = append(actors, vsphereTopologyActor("vsphere_resource_pool", pool.ID, pool.Name, map[string]any{
202 + "overall_status": pool.OverallStatus,
203 + "cpu_limit_mhz": pool.CpuLimit,
204 + "mem_limit_mb": pool.MemLimit,
205 + }, map[string]string{
206 + "datacenter": pool.Hier.DC.Name,
207 + "cluster": pool.Hier.Cluster.Name,
208 + "resource_pool": pool.Name,
209 + }))
210 + if c.resources.Clusters.Get(pool.Hier.Cluster.ID) != nil {
211 + links = append(links, vsphereTopologyLink(pool.Hier.Cluster.ID, pool.ID, "contains", "Cluster contains resource pool"))
212 + }
213 + }
214 +
215 + sort.SliceStable(actors, func(i, j int) bool { return actors[i].ActorID < actors[j].ActorID })
216 + sort.SliceStable(links, func(i, j int) bool {
217 + if links[i].SrcActorID != links[j].SrcActorID {
218 + return links[i].SrcActorID < links[j].SrcActorID
219 + }
220 + if links[i].DstActorID != links[j].DstActorID {
221 + return links[i].DstActorID < links[j].DstActorID
222 + }
223 + return links[i].LinkType < links[j].LinkType
224 + })
225 +
226 + return topology.Data{
227 + SchemaVersion: vsphereTopologySchemaVersion,
228 + Source: vsphereTopologySource,
229 + Layer: vsphereTopologyLayer,
230 + AgentID: strings.TrimSpace(agentID),
231 + CollectedAt: time.Now().UTC(),
232 + View: vsphereTopologyView,
233 + Actors: actors,
234 + Links: links,
235 + Stats: map[string]any{
236 + "datacenters": len(c.resources.DataCenters),
237 + "clusters": len(c.resources.Clusters),
238 + "hosts": len(c.resources.Hosts),
239 + "vms": len(c.resources.VMs),
240 + "datastores": len(c.resources.Datastores),
241 + "networks": len(c.resources.Networks),
242 + "datastore_clusters": len(c.resources.StoragePods),
243 + "resource_pools": len(c.resources.ResourcePools),
244 + "actors": len(actors),
245 + "links": len(links),
246 + },
247 + }, true
248 +}
249 +
250 +func vsphereTopologyActor(actorType, id, name string, attrs map[string]any, labels map[string]string) topology.Actor {
251 + attrs = cleanTopologyAnyMap(attrs)
252 + attrs["name"] = name
253 + attrs["vsphere_id"] = id
254 +
255 + return topology.Actor{
256 + ActorID: vsphereTopologyActorID(actorType, id),
257 + ActorType: actorType,
258 + Layer: vsphereTopologyLayer,
259 + Source: vsphereTopologySource,
260 + Match: topology.Match{},
261 + Attributes: attrs,
262 + Labels: cleanTopologyStringMap(labels),
263 + }
264 +}
265 +
266 +func vsphereTopologyLink(srcID, dstID, linkType, label string) topology.Link {
267 + srcActorID := vsphereTopologyActorIDForResource(srcID)
268 + dstActorID := vsphereTopologyActorIDForResource(dstID)
269 + return topology.Link{
270 + Layer: vsphereTopologyLayer,
271 + Protocol: vsphereTopologySource,
272 + LinkType: linkType,
273 + Direction: "parent_to_child",
274 + SrcActorID: srcActorID,
275 + DstActorID: dstActorID,
276 + Src: vsphereTopologyEndpoint(srcActorID),
277 + Dst: vsphereTopologyEndpoint(dstActorID),
278 + Metrics: map[string]any{
279 + "label": label,
280 + },
281 + }
282 +}
283 +
284 +func vsphereTopologyEndpoint(actorID string) topology.LinkEndpoint {
285 + return topology.LinkEndpoint{
286 + Match: topology.Match{},
287 + Attributes: map[string]any{
288 + "actor_id": actorID,
289 + },
290 + }
291 +}
292 +
293 +func vsphereTopologyActorID(actorType, id string) string {
294 + return actorType + ":" + id
295 +}
296 +
297 +func vsphereTopologyActorIDForResource(id string) string {
298 + switch {
299 + case strings.HasPrefix(id, "datacenter-"):
300 + return vsphereTopologyActorID("vsphere_datacenter", id)
301 + case strings.HasPrefix(id, "domain-"):
302 + return vsphereTopologyActorID("vsphere_cluster", id)
303 + case strings.HasPrefix(id, "host-"):
304 + return vsphereTopologyActorID("vsphere_host", id)
305 + case strings.HasPrefix(id, "vm-"):
306 + return vsphereTopologyActorID("vsphere_vm", id)
307 + case strings.HasPrefix(id, "datastore-"):
308 + return vsphereTopologyActorID("vsphere_datastore", id)
309 + case strings.HasPrefix(id, "network-"), strings.HasPrefix(id, "dvportgroup-"), strings.HasPrefix(id, "opaqueNetwork-"):
310 + return vsphereTopologyActorID("vsphere_network", id)
311 + case strings.HasPrefix(id, "group-p"):
312 + return vsphereTopologyActorID("vsphere_datastore_cluster", id)
313 + case strings.HasPrefix(id, "resgroup-"):
314 + return vsphereTopologyActorID("vsphere_resource_pool", id)
315 + default:
316 + return id
317 + }
318 +}
319 +
320 +func optionalBool(value *bool) any {
321 + if value == nil {
322 + return nil
323 + }
324 + return *value
325 +}
326 +
327 +func cleanTopologyStringMap(in map[string]string) map[string]string {
328 + out := make(map[string]string, len(in))
329 + for k, v := range in {
330 + k = strings.TrimSpace(k)
331 + v = strings.TrimSpace(v)
332 + if k != "" && v != "" {
333 + out[k] = v
334 + }
335 + }
336 + if len(out) == 0 {
337 + return nil
338 + }
339 + return out
340 +}
341 +
342 +func cleanTopologyAnyMap(in map[string]any) map[string]any {
343 + out := make(map[string]any, len(in)+2)
344 + for k, v := range in {
345 + k = strings.TrimSpace(k)
346 + if k == "" || v == nil {
347 + continue
348 + }
349 + if s, ok := v.(string); ok {
350 + s = strings.TrimSpace(s)
351 + if s == "" {
352 + continue
353 + }
354 + out[k] = s
355 + continue
356 + }
357 + out[k] = v
358 + }
359 + return out
360 +}
361 +
362 +func topologyActorCount(resources *rs.Resources) int {
363 + return len(resources.DataCenters) + len(resources.Clusters) + len(resources.Hosts) + len(resources.VMs) +
364 + len(resources.Datastores) + len(resources.Networks) + len(resources.StoragePods) + len(resources.ResourcePools)
365 +}
366 +
367 +func topologyLinkCount(resources *rs.Resources) int {
368 + count := len(resources.Clusters) + len(resources.Hosts) + len(resources.VMs) +
369 + len(resources.Datastores) + len(resources.Networks) + len(resources.StoragePods) + len(resources.ResourcePools)
370 + for _, network := range resources.Networks {
371 + count += len(network.HostIDs) + len(network.VMIDs)
372 + }
373 + return count
374 +}
src/go/plugin/go.d/collector/vsphere/func_topology_presentation.go new
+83
@@ -0,0 +1,83 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import "github.com/netdata/netdata/go/plugins/pkg/topology"
6 +
7 +func vsphereTopologyPresentation() *topology.Presentation {
8 + return &topology.Presentation{
9 + ActorTypes: map[string]topology.PresentationActorType{
10 + "vsphere_datacenter": vsphereActorType("Datacenter", "blue", "name", "vsphere_id"),
11 + "vsphere_cluster": vsphereActorType("Cluster", "green", "name", "overall_status", "drs_enabled", "ha_enabled", "vsan_enabled"),
12 + "vsphere_host": vsphereActorType("ESXi Host", "orange", "name", "connection_state", "power_state", "overall_status"),
13 + "vsphere_vm": vsphereActorType("VM", "purple", "name", "connection_state", "power_state", "snapshot_count"),
14 + "vsphere_datastore": vsphereActorType("Datastore", "cyan", "name", "type", "accessible", "maintenance_mode"),
15 + "vsphere_network": vsphereActorType("Network", "yellow", "name", "type", "accessible", "hosts", "vms"),
16 + "vsphere_datastore_cluster": vsphereActorType("Datastore Cluster", "teal", "name", "storage_drs_enabled"),
17 + "vsphere_resource_pool": vsphereActorType("Resource Pool", "gray", "name", "overall_status"),
18 + },
19 + LinkTypes: map[string]topology.PresentationLinkType{
20 + "contains": {Label: "Contains", ColorSlot: "gray", Width: 1},
21 + "connects": {Label: "Connects",
22 + ColorSlot: "green", Width: 1},
23 + "runs": {Label: "Runs", ColorSlot: "blue", Width: 1},
24 + },
25 + Legend: topology.PresentationLegend{
26 + Actors: []topology.PresentationLegendEntry{
27 + {Type: "vsphere_datacenter", Label: "Datacenter"},
28 + {Type: "vsphere_cluster", Label: "Cluster"},
29 + {Type: "vsphere_host", Label: "ESXi Host"},
30 + {Type: "vsphere_vm", Label: "VM"},
31 + {Type: "vsphere_datastore", Label: "Datastore"},
32 + {Type: "vsphere_network", Label: "Network"},
33 + {Type: "vsphere_datastore_cluster", Label: "Datastore Cluster"},
34 + {Type: "vsphere_resource_pool", Label: "Resource Pool"},
35 + },
36 + Links: []topology.PresentationLegendEntry{
37 + {Type: "contains", Label: "Contains"},
38 + {Type: "connects", Label: "Connects"},
39 + {Type: "runs", Label: "Runs"},
40 + },
41 + },
42 + ActorClickBehavior: "highlight_connections",
43 + }
44 +}
45 +
46 +func vsphereActorType(label, colorSlot string, summaryKeys ...string) topology.PresentationActorType {
47 + return topology.PresentationActorType{
48 + Label: label,
49 + ColorSlot: colorSlot,
50 + Border: true,
51 + SummaryFields: vsphereSummaryFields(summaryKeys...),
52 + }
53 +}
54 +
55 +func vsphereSummaryFields(keys ...string) []topology.PresentationSummaryField {
56 + fields := make([]topology.PresentationSummaryField, 0, len(keys))
57 + for _, key := range keys {
58 + fields = append(fields, topology.PresentationSummaryField{
59 + Key: key,
60 + Label: vsphereSummaryFieldLabels[key],
61 + Sources: []string{"attributes"},
62 + })
63 + }
64 + return fields
65 +}
66 +
67 +var vsphereSummaryFieldLabels = map[string]string{
68 + "accessible": "Accessible",
69 + "connection_state": "Connection State",
70 + "drs_enabled": "DRS Enabled",
71 + "ha_enabled": "HA Enabled",
72 + "hosts": "Hosts",
73 + "maintenance_mode": "Maintenance Mode",
74 + "name": "Name",
75 + "overall_status": "Overall Status",
76 + "power_state": "Power State",
77 + "snapshot_count": "Snapshots",
78 + "storage_drs_enabled": "Storage DRS Enabled",
79 + "type": "Type",
80 + "vms": "VMs",
81 + "vsan_enabled": "vSAN Enabled",
82 + "vsphere_id": "vSphere ID",
83 +}
src/go/plugin/go.d/collector/vsphere/func_topology_test.go new
+255
@@ -0,0 +1,255 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import (
6 + "context"
7 + "testing"
8 +
9 + "github.com/netdata/netdata/go/plugins/pkg/topology"
10 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
11 +
12 + "github.com/stretchr/testify/require"
13 +)
14 +
15 +func TestFuncTopology_Handle(t *testing.T) {
16 + tests := map[string]struct {
17 + method string
18 + collector func() *Collector
19 + want int
20 + check func(*testing.T, any)
21 + }{
22 + "without discovery": {
23 + method: "topology:vsphere",
24 + collector: New,
25 + want: 503,
26 + },
27 + "with empty inventory cache": {
28 + method: "topology:vsphere",
29 + collector: func() *Collector {
30 + collr := New()
31 + collr.resources = &rs.Resources{}
32 + return collr
33 + },
34 + want: 200,
35 + check: func(t *testing.T, raw any) {
36 + data, ok := raw.(topology.Data)
37 + require.True(t, ok)
38 + require.Empty(t, data.Actors)
39 + require.Empty(t, data.Links)
40 + require.EqualValues(t, 0, data.Stats["hosts"])
41 + require.EqualValues(t, 0, data.Stats["vms"])
42 + },
43 + },
44 + "unknown method": {
45 + method: "unknown",
46 + collector: New,
47 + want: 404,
48 + },
49 + }
50 +
51 + for name, tc := range tests {
52 + t.Run(name, func(t *testing.T) {
53 + handler := &funcTopology{collector: tc.collector(), agentID: "vsphere_vcenter1"}
54 +
55 + resp := handler.Handle(context.Background(), tc.method, nil)
56 +
57 + require.Equal(t, tc.want, resp.Status)
58 + if tc.check != nil {
59 + tc.check(t, resp.Data)
60 + }
61 + })
62 + }
63 +}
64 +
65 +func TestFuncTopology_HandleWithInventoryCache(t *testing.T) {
66 + collr := New()
67 + collr.resources = &rs.Resources{
68 + DataCenters: rs.DataCenters{
69 + "datacenter-1": {ID: "datacenter-1", Name: "DC1"},
70 + },
71 + Clusters: rs.Clusters{
72 + "domain-c1": {
73 + ID: "domain-c1",
74 + Name: "Cluster1",
75 + Hier: rs.ClusterHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}},
76 + OverallStatus: "green",
77 + DrsEnabled: true,
78 + HaEnabled: true,
79 + },
80 + },
81 + Hosts: rs.Hosts{
82 + "host-1": {
83 + ID: "host-1",
84 + Name: "Host1",
85 + Hier: rs.HostHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c1", Name: "Cluster1"}},
86 + ConnectionState: "connected",
87 + PowerState: "poweredOn",
88 + OverallStatus: "green",
89 + },
90 + },
91 + VMs: rs.VMs{
92 + "vm-1": {
93 + ID: "vm-1",
94 + Name: "VM1",
95 + Hier: rs.VMHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c1", Name: "Cluster1"}, Host: rs.HierarchyValue{ID: "host-1", Name: "Host1"}},
96 + ConnectionState: "connected",
97 + PowerState: "poweredOn",
98 + OverallStatus: "green",
99 + SnapshotCount: 2,
100 + SnapshotMaxChainDepth: 3,
101 + },
102 + },
103 + Datastores: rs.Datastores{
104 + "datastore-1": {
105 + ID: "datastore-1",
106 + Name: "Datastore1",
107 + Hier: rs.DatastoreHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}},
108 + Type: "VMFS",
109 + Accessible: true,
110 + MaintenanceMode: "normal",
111 + Capacity: 1000,
112 + FreeSpace: 400,
113 + },
114 + },
115 + Networks: rs.Networks{
116 + "network-1": {
117 + ID: "network-1",
118 + Name: "VM Network",
119 + Type: "Network",
120 + Hier: rs.NetworkHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}},
121 + Accessible: true,
122 + HostIDs: []string{"host-1"},
123 + VMIDs: []string{"vm-1"},
124 + OverallStatus: "green",
125 + },
126 + },
127 + StoragePods: rs.StoragePods{
128 + "group-p1": {
129 + ID: "group-p1",
130 + Name: "Pod1",
131 + Hier: rs.StoragePodHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}},
132 + StorageDRSEnabled: new(true),
133 + },
134 + },
135 + ResourcePools: rs.ResourcePools{
136 + "resgroup-1": {
137 + ID: "resgroup-1",
138 + Name: "Resources",
139 + Hier: rs.ResourcePoolHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c1", Name: "Cluster1"}},
140 + OverallStatus: "green",
141 + },
142 + },
143 + }
144 + handler := &funcTopology{collector: collr, agentID: "vsphere_vcenter1"}
145 +
146 + resp := handler.Handle(context.Background(), "topology:vsphere", nil)
147 +
148 + require.Equal(t, 200, resp.Status)
149 + require.Equal(t, "topology", resp.ResponseType)
150 + data, ok := resp.Data.(topology.Data)
151 + require.True(t, ok)
152 + require.Equal(t, "vsphere", data.Source)
153 + require.Equal(t, "virtualization", data.Layer)
154 + require.Equal(t, "inventory", data.View)
155 + require.Equal(t, "vsphere_vcenter1", data.AgentID)
156 + require.Len(t, data.Actors, 8)
157 + require.Len(t, data.Links, 9)
158 +
159 + actors := topologyActorsByID(data.Actors)
160 + require.Contains(t, actors, "vsphere_datacenter:datacenter-1")
161 + require.Contains(t, actors, "vsphere_cluster:domain-c1")
162 + require.Contains(t, actors, "vsphere_host:host-1")
163 + require.Contains(t, actors, "vsphere_vm:vm-1")
164 + require.Contains(t, actors, "vsphere_network:network-1")
165 + require.Equal(t, "VM1", actors["vsphere_vm:vm-1"].Attributes["name"])
166 + require.EqualValues(t, 2, actors["vsphere_vm:vm-1"].Attributes["snapshot_count"])
167 + require.Equal(t, "VM Network", actors["vsphere_network:network-1"].Attributes["name"])
168 +
169 + require.Contains(t, topologyLinkKeys(data.Links), "vsphere_host:host-1->vsphere_vm:vm-1:runs")
170 + require.Contains(t, topologyLinkKeys(data.Links), "vsphere_host:host-1->vsphere_network:network-1:connects")
171 + require.Contains(t, topologyLinkKeys(data.Links), "vsphere_vm:vm-1->vsphere_network:network-1:connects")
172 +}
173 +
174 +func TestVSphereTopologyActorIDForResource(t *testing.T) {
175 + tests := map[string]struct {
176 + id string
177 + want string
178 + }{
179 + "opaque network": {
180 + id: "opaqueNetwork-1",
181 + want: "vsphere_network:opaqueNetwork-1",
182 + },
183 + "standard network": {
184 + id: "network-1",
185 + want: "vsphere_network:network-1",
186 + },
187 + "distributed port group": {
188 + id: "dvportgroup-1",
189 + want: "vsphere_network:dvportgroup-1",
190 + },
191 + }
192 +
193 + for name, tc := range tests {
194 + t.Run(name, func(t *testing.T) {
195 + require.Equal(t, tc.want, vsphereTopologyActorIDForResource(tc.id))
196 + })
197 + }
198 +}
199 +
200 +func TestFuncTopology_DoesNotLinkToFilteredActors(t *testing.T) {
201 + collr := New()
202 + collr.resources = &rs.Resources{
203 + DataCenters: rs.DataCenters{
204 + "datacenter-1": {ID: "datacenter-1", Name: "DC1"},
205 + },
206 + Clusters: rs.Clusters{},
207 + Hosts: rs.Hosts{
208 + "host-1": {
209 + ID: "host-1",
210 + Name: "Host1",
211 + Hier: rs.HostHierarchy{
212 + DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"},
213 + Cluster: rs.HierarchyValue{ID: "domain-c-filtered", Name: "Filtered"},
214 + },
215 + },
216 + },
217 + VMs: rs.VMs{
218 + "vm-1": {
219 + ID: "vm-1",
220 + Name: "VM1",
221 + Hier: rs.VMHierarchy{
222 + DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"},
223 + Cluster: rs.HierarchyValue{ID: "domain-c-filtered", Name: "Filtered"},
224 + Host: rs.HierarchyValue{ID: "host-filtered", Name: "FilteredHost"},
225 + },
226 + },
227 + },
228 + }
229 +
230 + data, ok := collr.topologyData("agent")
231 +
232 + require.True(t, ok)
233 + require.Len(t, data.Actors, 3)
234 + keys := topologyLinkKeys(data.Links)
235 + require.Contains(t, keys, "vsphere_datacenter:datacenter-1->vsphere_host:host-1:contains")
236 + require.Contains(t, keys, "vsphere_datacenter:datacenter-1->vsphere_vm:vm-1:contains")
237 + require.NotContains(t, keys, "vsphere_cluster:domain-c-filtered->vsphere_host:host-1:contains")
238 + require.NotContains(t, keys, "vsphere_host:host-filtered->vsphere_vm:vm-1:runs")
239 +}
240 +
241 +func topologyActorsByID(actors []topology.Actor) map[string]topology.Actor {
242 + out := make(map[string]topology.Actor, len(actors))
243 + for _, actor := range actors {
244 + out[actor.ActorID] = actor
245 + }
246 + return out
247 +}
248 +
249 +func topologyLinkKeys(links []topology.Link) map[string]struct{} {
250 + out := make(map[string]struct{}, len(links))
251 + for _, link := range links {
252 + out[link.SrcActorID+"->"+link.DstActorID+":"+link.LinkType] = struct{}{}
253 + }
254 + return out
255 +}
src/go/plugin/go.d/collector/vsphere/init.go
+84 -7
@@ -4,9 +4,11 @@ package vsphere
4
5 import (
6 "errors"
7 + "fmt"
8
9 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/client"
10 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/discover"
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/match"
12 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/scrape"
13 )
14
@@ -14,14 +16,83 @@ func (c *Collector) validateConfig() error {
16 const minRecommendedUpdateEvery = 20
17
18 if c.URL == "" {
17 - return errors.New("URL is not set")
19 + return errors.New("config option url is required")
20 }
21 if c.Username == "" || c.Password == "" {
20 - return errors.New("username or password not set")
22 + return errors.New("config options username and password are required")
23 }
24 if c.UpdateEvery < minRecommendedUpdateEvery {
23 - c.Warningf("update_every is to low, minimum recommended is %d", minRecommendedUpdateEvery)
25 + c.Warningf("config option update_every=%d is lower than recommended minimum %d", c.UpdateEvery, minRecommendedUpdateEvery)
26 }
27 + if c.DiscoveryInterval.Duration() <= 0 {
28 + return errors.New("config option discovery_interval must be greater than zero")
29 + }
30 + if len(c.TagCategories) > 0 {
31 + m, err := match.NewPatternListMatcher("tag_categories", c.TagCategories)
32 + if err != nil {
33 + return err
34 + }
35 + c.vsphereTagCategoryMatcher = m
36 + }
37 + if len(c.CustomAttributes) > 0 {
38 + m, err := match.NewPatternListMatcher("custom_attributes", c.CustomAttributes)
39 + if err != nil {
40 + return err
41 + }
42 + c.customAttributeMatcher = m
43 + }
44 + if err := c.validateDatastoreClusterConfig(); err != nil {
45 + return err
46 + }
47 + if err := c.validateVSANConfig(); err != nil {
48 + return err
49 + }
50 + return nil
51 +}
52 +
53 +func (c *Collector) validateDatastoreClusterConfig() error {
54 + if !c.CollectDatastoreClusters {
55 + return nil
56 + }
57 + if len(c.DatastoreClustersInclude) == 0 {
58 + c.DatastoreClustersInclude = match.DatastoreClusterIncludes{"/*"}
59 + }
60 + m, err := c.DatastoreClustersInclude.Parse()
61 + if err != nil {
62 + return err
63 + }
64 + c.datastoreClusterMatcher = m
65 + return nil
66 +}
67 +
68 +func (c *Collector) validateVSANConfig() error {
69 + if !c.CollectVSAN {
70 + return nil
71 + }
72 + if len(c.VSANClustersInclude) == 0 {
73 + c.VSANClustersInclude = match.VSANClusterIncludes{"/*"}
74 + }
75 + vsanClusterMatcher, err := c.VSANClustersInclude.Parse()
76 + if err != nil {
77 + return err
78 + }
79 + c.vsanClusterMatcher = vsanClusterMatcher
80 + if len(c.VSANHostsInclude) == 0 {
81 + c.VSANHostsInclude = match.VSANHostIncludes{"/*"}
82 + }
83 + vsanHostMatcher, err := c.VSANHostsInclude.Parse()
84 + if err != nil {
85 + return err
86 + }
87 + c.vsanHostMatcher = vsanHostMatcher
88 + if len(c.VSANVMsInclude) == 0 {
89 + c.VSANVMsInclude = match.VSANVMIncludes{"/*"}
90 + }
91 + vsanVMMatcher, err := c.VSANVMsInclude.Parse()
92 + if err != nil {
93 + return err
94 + }
95 + c.vsanVMMatcher = vsanVMMatcher
96 return nil
97 }
98
@@ -39,24 +110,30 @@ func (c *Collector) initClient() (*client.Client, error) {
110 func (c *Collector) initDiscoverer(cli *client.Client) error {
111 d := discover.New(cli)
112 d.Logger = c.Logger
113 + d.CollectDatastoreClusters = c.CollectDatastoreClusters
114 + d.CollectVSAN = c.CollectVSAN
115 + d.CollectNetworkTopology = c.CollectNetworkTopology
116 + d.DatastoreClusterMatcher = c.datastoreClusterMatcher
117 + d.TagCategoryMatcher = c.vsphereTagCategoryMatcher
118 + d.CustomAttributeMatcher = c.customAttributeMatcher
119
120 hm, err := c.HostsInclude.Parse()
121 if err != nil {
45 - return err
122 + return fmt.Errorf("parse config option host_include: %w", err)
123 }
124 if hm != nil {
125 d.HostMatcher = hm
126 }
127 vmm, err := c.VMsInclude.Parse()
128 if err != nil {
52 - return err
129 + return fmt.Errorf("parse config option vm_include: %w", err)
130 }
131 if vmm != nil {
132 d.VMMatcher = vmm
133 }
134 dsm, err := c.DatastoresInclude.Parse()
135 if err != nil {
59 - return err
136 + return fmt.Errorf("parse config option datastore_include: %w", err)
137 }
138 if dsm != nil {
139 d.DatastoreMatcher = dsm
@@ -64,7 +141,7 @@ func (c *Collector) initDiscoverer(cli *client.Client) error {
141
142 cm, err := c.ClustersInclude.Parse()
143 if err != nil {
67 - return err
144 + return fmt.Errorf("parse config option cluster_include: %w", err)
145 }
146 if cm != nil {
147 d.ClusterMatcher = cm
src/go/plugin/go.d/collector/vsphere/integrations/vmware_vcenter_server.md
+447 -15
@@ -21,7 +21,17 @@ Module: vsphere
21
22 ## Overview
23
24 -This collector monitors hosts, VMs, datastores, clusters, and resource pools from `vCenter` servers.
24 +Monitors vSphere resources from `vCenter` servers.
25 +
26 +Includes hosts, VMs, datastores, clusters, resource pools,
27 +and inventory counts.
28 +
29 +Use the `vcsa` collector for vCenter Server Appliance health.
30 +
31 +Use the `snmp` collector with the `vmware-esx` profile for
32 +ESXi hardware, HBA, and environment sensors.
33 +
34 +Those surfaces are intentionally not duplicated here by default.
35
36 > **Warning**: The `vsphere` collector cannot re-login and continue collecting metrics after a vCenter reboot.
37 > go.d.plugin needs to be restarted.
@@ -126,31 +136,165 @@ The following options can be defined globally: update_every, autodetection_retry
136 | Group | Option | Description | Default | Required |
137 |:------|:-----|:------------|:--------|:---------:|
138 | **Collection** | update_every | Data collection interval (seconds). | 20 | no |
129 -| | autodetection_retry | Autodetection retry interval (seconds). Set 0 to disable. | 0 | no |
139 +| | autodetection_retry | Autodetection retry interval (seconds). | 60 | no |
140 | **Target** | url | Target endpoint URL. | https://vcenter.local | yes |
141 | | timeout | HTTP request timeout (seconds). | 20 | no |
142 | **Discovery** | discovery_interval | Hosts, VMs, datastores, clusters, and resource pools discovery interval (seconds). | 300 | no |
143 +| **Labels** | [tag_categories](#option-labels-tag-categories) | vSphere tag category allowlist. | | no |
144 +| | [custom_attributes](#option-labels-custom-attributes) | vSphere custom attribute allowlist. | | no |
145 +| **High Cardinality** | [collect_datastore_clusters](#option-high-cardinality-collect-datastore-clusters) | Collect datastore cluster capacity and Storage DRS status. | no | no |
146 +| | [datastore_cluster_include](#option-high-cardinality-datastore-cluster-include) | Datastore cluster selector. | /* | no |
147 +| | [collect_vsan](#option-high-cardinality-collect-vsan) | Collect vSAN metrics. | no | no |
148 +| | [vsan_cluster_include](#option-high-cardinality-vsan-cluster-include) | vSAN cluster selector. | /* | no |
149 +| | [vsan_host_include](#option-high-cardinality-vsan-host-include) | vSAN host selector. | /* | no |
150 +| | [vsan_vm_include](#option-high-cardinality-vsan-vm-include) | vSAN VM selector. | /* | no |
151 +| **Collection** | [collect_network_topology](#option-collection-collect-network-topology) | Discover networks for the vSphere Topology function. | no | no |
152 | **Filters** | [host_include](#option-filters-host-include) | Hosts selector (filter). | /* | no |
153 | | [vm_include](#option-filters-vm-include) | VM selector (filter). | /* | no |
154 | | [datastore_include](#option-filters-datastore-include) | Datastore selector (filter). | /* | no |
155 | | [cluster_include](#option-filters-cluster-include) | Cluster selector (filter). Resource pools follow their owning cluster. | /* | no |
156 | **HTTP Auth** | username | Username for Basic HTTP authentication. | | yes |
157 | | password | Password for Basic HTTP authentication. | | yes |
139 -| | bearer_token_file | Path to a file containing a bearer token (used for `Authorization: Bearer`). | | no |
158 | **TLS** | tls_skip_verify | Skip TLS certificate and hostname verification (insecure). | no | no |
159 | | tls_ca | Path to CA bundle used to validate the server certificate. | | no |
160 | | tls_cert | Path to client TLS certificate (for mTLS). | | no |
161 | | tls_key | Path to client TLS private key (for mTLS). | | no |
144 -| **Proxy** | proxy_url | HTTP proxy URL. | | no |
145 -| | proxy_username | Username for proxy Basic HTTP authentication. | | no |
146 -| | proxy_password | Password for proxy Basic HTTP authentication. | | no |
147 -| **Request** | method | HTTP method to use. | GET | no |
148 -| | body | Request body (e.g., for POST/PUT). | | no |
149 -| | headers | Additional HTTP headers (one per line as key: value). | | no |
150 -| | not_follow_redirects | Do not follow HTTP redirects. | no | no |
151 -| | force_http2 | Force HTTP/2 (including h2c over TCP). | no | no |
162 | **Virtual Node** | vnode | Associates this data collection job with a [Virtual Node](https://learn.netdata.cloud/docs/netdata-agent/configuration/organize-systems-metrics-and-alerts#virtual-nodes). | | no |
163
164 +<a id="option-labels-tag-categories"></a>
165 +##### tag_categories
166 +
167 +Disabled by default because vSphere tags are user-defined metadata
168 +and can expose internal names, ownership, business unit, or
169 +environment details. Each list item is one glob pattern matching
170 +vSphere tag category names, so names with spaces are supported.
171 +Use `*` only when every tag category is intentional.
172 +
173 +Matching categories are exposed as labels named
174 +`vsphere_tag_<sanitized_category>`. When a resource has multiple
175 +tags in the same category, values are sorted and joined with the
176 +pipe character.
177 +
178 +```yaml
179 +tag_categories:
180 + - "Environment"
181 + - "Business Unit"
182 +```
183 +
184 +
185 +<a id="option-labels-custom-attributes"></a>
186 +##### custom_attributes
187 +
188 +Disabled by default because vSphere custom attributes are
189 +user-defined metadata and can expose internal names, ownership,
190 +business unit, operational data, or secrets stored by administrators.
191 +Custom attribute values are sent verbatim as labels. Each list
192 +item is one glob pattern matching custom attribute names, so names
193 +with spaces are supported. Use `*` only when every custom attribute
194 +is intentional and none of the matched values contain secrets.
195 +
196 +Matching attributes are exposed as labels named
197 +`vsphere_custom_attribute_<sanitized_name>`.
198 +
199 +```yaml
200 +custom_attributes:
201 + - "Owner"
202 + - "Cost Center"
203 +```
204 +
205 +
206 +<a id="option-high-cardinality-collect-datastore-clusters"></a>
207 +##### collect_datastore_clusters
208 +
209 +Disabled by default because it adds a separate vSphere resource
210 +class (`StoragePod`) to the collector output. When enabled, the
211 +collector emits aggregate datastore-cluster capacity, utilization,
212 +and Storage DRS status.
213 +
214 +
215 +<a id="option-high-cardinality-datastore-cluster-include"></a>
216 +##### datastore_cluster_include
217 +
218 +Applies only when `collect_datastore_clusters` is enabled. Values
219 +use Netdata simple patterns and match
220 +`/Datacenter/DatastoreCluster`, the datastore-cluster name, or
221 +the vSphere managed object ID. Matching datastore clusters are
222 +included in metrics, labels, cached discovery state, and topology
223 +function output.
224 +
225 +```yaml
226 +datastore_cluster_include:
227 + - "/*"
228 +```
229 +
230 +
231 +<a id="option-high-cardinality-collect-vsan"></a>
232 +##### collect_vsan
233 +
234 +Disabled by default because it uses the vSAN Management API and
235 +vSAN Performance Service, and adds extra vCenter queries. When
236 +enabled, it emits vSAN cluster capacity, vSAN cluster health, and
237 +vSAN cluster, host, and VM performance metrics for discovered
238 +vSAN-enabled clusters. Use the vSAN selectors below to choose the
239 +concrete vSAN performance entity refs queried. vSAN events are
240 +not collected by this option.
241 +
242 +
243 +<a id="option-high-cardinality-vsan-cluster-include"></a>
244 +##### vsan_cluster_include
245 +
246 +Applies only when `collect_vsan` is enabled. Values use Netdata
247 +simple patterns and match `/Datacenter/Cluster`, the cluster
248 +name, the vSphere managed object ID, or `vsan_uuid:<uuid>`.
249 +
250 +```yaml
251 +vsan_cluster_include:
252 + - "/*"
253 + - "vsan_uuid:52b..."
254 +```
255 +
256 +
257 +<a id="option-high-cardinality-vsan-host-include"></a>
258 +##### vsan_host_include
259 +
260 +Applies only when `collect_vsan` is enabled. Values use Netdata
261 +simple patterns and match `/Datacenter/Cluster/Host`, the host
262 +name, the vSphere managed object ID, or
263 +`vsan_node_uuid:<uuid>`.
264 +
265 +```yaml
266 +vsan_host_include:
267 + - "/*"
268 + - "vsan_node_uuid:52b..."
269 +```
270 +
271 +
272 +<a id="option-high-cardinality-vsan-vm-include"></a>
273 +##### vsan_vm_include
274 +
275 +Applies only when `collect_vsan` is enabled. Values use Netdata
276 +simple patterns and match `/Datacenter/Cluster/Host/VM`, the VM
277 +name, the vSphere managed object ID, or
278 +`instance_uuid:<uuid>`.
279 +
280 +```yaml
281 +vsan_vm_include:
282 + - "/*"
283 + - "instance_uuid:52b..."
284 +```
285 +
286 +
287 +<a id="option-collection-collect-network-topology"></a>
288 +##### collect_network_topology
289 +
290 +Disabled by default to avoid extra vCenter discovery calls for
291 +existing users. When enabled, the collector discovers vSphere
292 +Network and Distributed Virtual Port Group objects and includes
293 +their cached accessibility/status and host/VM relationships in
294 +the vSphere Topology function. It does not create charts or
295 +metrics.
296 +
297 +
298 <a id="option-filters-host-include"></a>
299 ##### host_include
300
@@ -304,7 +448,9 @@ The following alerts are available:
448 | Alert name | On metric | Description |
449 |:------------|:----------|:------------|
450 | [ vsphere_vm_cpu_utilization ](https://github.com/netdata/netdata/blob/master/src/health/health.d/vsphere.conf) | vsphere.vm_cpu_utilization | Virtual Machine CPU utilization |
307 -| [ vsphere_vm_mem_usage ](https://github.com/netdata/netdata/blob/master/src/health/health.d/vsphere.conf) | vsphere.vm_mem_utilization | Virtual Machine memory utilization |
451 +| [ vsphere_vm_mem_utilization ](https://github.com/netdata/netdata/blob/master/src/health/health.d/vsphere.conf) | vsphere.vm_mem_utilization | Virtual Machine memory utilization |
452 +| [ vsphere_vm_snapshot_chain_depth ](https://github.com/netdata/netdata/blob/master/src/health/health.d/vsphere.conf) | vsphere.vm_snapshot_max_chain_depth | Virtual Machine snapshot maximum chain depth |
453 +| [ vsphere_vm_snapshot_age ](https://github.com/netdata/netdata/blob/master/src/health/health.d/vsphere.conf) | vsphere.vm_snapshot_max_age | Virtual Machine oldest snapshot age |
454 | [ vsphere_host_cpu_utilization ](https://github.com/netdata/netdata/blob/master/src/health/health.d/vsphere.conf) | vsphere.host_cpu_utilization | ESXi Host CPU utilization |
455 | [ vsphere_host_mem_utilization ](https://github.com/netdata/netdata/blob/master/src/health/health.d/vsphere.conf) | vsphere.host_mem_utilization | ESXi Host memory utilization |
456
@@ -317,6 +463,22 @@ The scope defines the instance that the metric belongs to. An instance is unique
463
464
465
466 +### Per inventory
467 +
468 +These metrics refer to the discovered vSphere inventory for this collector job.
469 +
470 +Labels:
471 +
472 +| Label | Description |
473 +|:-----------|:----------------|
474 +| id | Static inventory instance ID |
475 +
476 +Metrics:
477 +
478 +| Metric | Dimensions | Unit |
479 +|:------|:----------|:----|
480 +| vsphere.inventory_objects | datacenters, folders, clusters, hosts, vms, datastores, resource_pools | objects |
481 +
482 ### Per virtual machine
483
484 These metrics refer to the Virtual Machine.
@@ -325,10 +487,13 @@ Labels:
487
488 | Label | Description |
489 |:-----------|:----------------|
490 +| id | vSphere managed object reference ID |
491 | datacenter | Datacenter name |
492 | cluster | Cluster name |
493 | host | Host name |
494 | vm | Virtual Machine name |
495 +| vsphere_tag_<category> | vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character |
496 +| vsphere_custom_attribute_<name> | vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys |
497
498 Metrics:
499
@@ -343,9 +508,119 @@ Metrics:
508 | vsphere.vm_disk_max_latency | latency | milliseconds |
509 | vsphere.vm_net_traffic | received, sent | KiB/s |
510 | vsphere.vm_net_packets | received, sent | packets |
346 -| vsphere.vm_net_drops | received, sent | packets |
511 +| vsphere.vm_net_drops | received, sent | drops |
512 | vsphere.vm_overall_status | green, red, yellow, gray | status |
513 +| vsphere.vm_power_state | powered_on, powered_off, suspended | status |
514 +| vsphere.vm_connection_state | connected, disconnected, orphaned, inaccessible, invalid | status |
515 +| vsphere.vm_tools_running_status | running, not_running, executing_scripts, unknown | status |
516 +| vsphere.vm_tools_version_status | current, need_upgrade, not_installed, unmanaged, too_old, supported_old, supported_new, too_new, blacklisted, unknown | status |
517 +| vsphere.vm_consolidation_needed | needed, not_needed | status |
518 | vsphere.vm_system_uptime | uptime | seconds |
519 +| vsphere.vm_config_cpu | vcpus | vCPUs |
520 +| vsphere.vm_config_memory | memory | MiB |
521 +| vsphere.vm_config_devices | disks, nics | devices |
522 +| vsphere.vm_storage_usage | committed, uncommitted, unshared | bytes |
523 +| vsphere.vm_snapshot_count | count | snapshots |
524 +| vsphere.vm_snapshot_max_age | age | seconds |
525 +| vsphere.vm_snapshot_max_chain_depth | depth | snapshots |
526 +
527 +### Per virtual machine power
528 +
529 +These aggregate metrics refer to VM power and energy and are collected for discovered powered-on VMs when vSphere exposes the corresponding power counters.
530 +
531 +Labels:
532 +
533 +| Label | Description |
534 +|:-----------|:----------------|
535 +| id | vSphere managed object reference ID of the VM |
536 +| datacenter | Datacenter name |
537 +| cluster | Cluster name |
538 +| host | Host name |
539 +| vm | Virtual Machine name |
540 +| vsphere_tag_<category> | vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character |
541 +| vsphere_custom_attribute_<name> | vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys |
542 +
543 +Metrics:
544 +
545 +| Metric | Dimensions | Unit |
546 +|:------|:----------|:----|
547 +| vsphere.vm_power_usage | power | watts |
548 +| vsphere.vm_energy_usage | energy | joules |
549 +
550 +### Per vSAN virtual machine
551 +
552 +These optional metrics refer to VM vSAN performance and are collected only when `collect_vsan` is enabled.
553 +
554 +Labels:
555 +
556 +| Label | Description |
557 +|:-----------|:----------------|
558 +| id | vSphere managed object reference ID of the VM |
559 +| datacenter | Datacenter name |
560 +| cluster | Cluster name |
561 +| host | Host name |
562 +| vm | Virtual Machine name |
563 +| vm_instance_uuid | VM instance UUID used by vSAN performance entity references |
564 +| vsphere_tag_<category> | vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character |
565 +| vsphere_custom_attribute_<name> | vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys |
566 +
567 +Metrics:
568 +
569 +| Metric | Dimensions | Unit |
570 +|:------|:----------|:----|
571 +| vsphere.vsan_vm_operations | read, write | operations/s |
572 +| vsphere.vsan_vm_throughput | read, write | bytes/s |
573 +| vsphere.vsan_vm_latency | read, write | microseconds |
574 +
575 +### Per host power
576 +
577 +These aggregate metrics refer to ESXi host power, energy, and power capacity and are collected for discovered powered-on hosts when vSphere exposes the corresponding power counters.
578 +
579 +Labels:
580 +
581 +| Label | Description |
582 +|:-----------|:----------------|
583 +| id | vSphere managed object reference ID of the host |
584 +| datacenter | Datacenter name |
585 +| cluster | Cluster name |
586 +| host | Host name |
587 +| vsphere_tag_<category> | vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character |
588 +| vsphere_custom_attribute_<name> | vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys |
589 +
590 +Metrics:
591 +
592 +| Metric | Dimensions | Unit |
593 +|:------|:----------|:----|
594 +| vsphere.host_power_usage | power, cap | watts |
595 +| vsphere.host_power_capacity_usage | used, usable, idle, system, vm | watts |
596 +| vsphere.host_power_capacity_utilization | used | percentage |
597 +| vsphere.host_energy_usage | energy | joules |
598 +
599 +### Per vSAN host
600 +
601 +These optional metrics refer to ESXi host vSAN performance and are collected only when `collect_vsan` is enabled.
602 +
603 +Labels:
604 +
605 +| Label | Description |
606 +|:-----------|:----------------|
607 +| id | vSphere managed object reference ID of the host |
608 +| datacenter | Datacenter name |
609 +| cluster | Cluster name |
610 +| host | Host name |
611 +| vsan_node_uuid | vSAN host node UUID used by vSAN performance entity references |
612 +| vsphere_tag_<category> | vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character |
613 +| vsphere_custom_attribute_<name> | vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys |
614 +
615 +Metrics:
616 +
617 +| Metric | Dimensions | Unit |
618 +|:------|:----------|:----|
619 +| vsphere.vsan_host_operations | read, write | operations/s |
620 +| vsphere.vsan_host_throughput | read, write | bytes/s |
621 +| vsphere.vsan_host_latency | read, write | microseconds |
622 +| vsphere.vsan_host_congestions | congestions | congestions/s |
623 +| vsphere.vsan_host_cache_hit_rate | hit_rate | percentage |
624
625 ### Per host
626
@@ -355,9 +630,12 @@ Labels:
630
631 | Label | Description |
632 |:-----------|:----------------|
633 +| id | vSphere managed object reference ID |
634 | datacenter | Datacenter name |
635 | cluster | Cluster name |
636 | host | Host name |
637 +| vsphere_tag_<category> | vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character |
638 +| vsphere_custom_attribute_<name> | vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys |
639
640 Metrics:
641
@@ -371,9 +649,12 @@ Metrics:
649 | vsphere.host_disk_max_latency | latency | milliseconds |
650 | vsphere.host_net_traffic | received, sent | KiB/s |
651 | vsphere.host_net_packets | received, sent | packets |
374 -| vsphere.host_net_drops | received, sent | packets |
652 +| vsphere.host_net_drops | received, sent | drops |
653 | vsphere.host_net_errors | received, sent | errors |
654 | vsphere.host_overall_status | green, red, yellow, gray | status |
655 +| vsphere.host_power_state | powered_on, powered_off, standby, unknown | status |
656 +| vsphere.host_connection_state | connected, not_responding, disconnected | status |
657 +| vsphere.host_maintenance_status | normal, in_maintenance | status |
658 | vsphere.host_system_uptime | uptime | seconds |
659
660 ### Per datastore
@@ -384,9 +665,12 @@ Labels:
665
666 | Label | Description |
667 |:-----------|:----------------|
668 +| id | vSphere managed object reference ID |
669 | datacenter | Datacenter name |
670 | datastore | Datastore name |
671 | type | Datastore type (VMFS, NFS, NFS41, vsan, VVOL, PMEM) |
672 +| vsphere_tag_<category> | vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character |
673 +| vsphere_custom_attribute_<name> | vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys |
674
675 Metrics:
676
@@ -396,8 +680,61 @@ Metrics:
680 | vsphere.datastore_disk_iops | reads, writes | operations/s |
681 | vsphere.datastore_disk_latency | read, write | milliseconds |
682 | vsphere.datastore_space_utilization | used | percentage |
399 -| vsphere.datastore_space_usage | capacity, free, used | bytes |
683 +| vsphere.datastore_space_usage | capacity, free, used, uncommitted | bytes |
684 | vsphere.datastore_overall_status | green, red, yellow, gray | status |
685 +| vsphere.datastore_accessibility_status | accessible, inaccessible | status |
686 +| vsphere.datastore_maintenance_status | normal, entering_maintenance, in_maintenance, unknown | status |
687 +| vsphere.datastore_multiple_host_access | enabled, disabled, unknown | status |
688 +
689 +### Per datastore cluster
690 +
691 +These optional metrics refer to datastore clusters (StoragePod objects) and are collected only when `collect_datastore_clusters` is enabled.
692 +
693 +Labels:
694 +
695 +| Label | Description |
696 +|:-----------|:----------------|
697 +| id | vSphere managed object reference ID |
698 +| datacenter | Datacenter name |
699 +| datastore_cluster | Datastore cluster name |
700 +| vsphere_tag_<category> | vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character |
701 +| vsphere_custom_attribute_<name> | vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys |
702 +
703 +Metrics:
704 +
705 +| Metric | Dimensions | Unit |
706 +|:------|:----------|:----|
707 +| vsphere.datastore_cluster_space_utilization | used | percentage |
708 +| vsphere.datastore_cluster_space_usage | capacity, free, used | bytes |
709 +| vsphere.datastore_cluster_storage_drs_status | enabled, disabled | status |
710 +| vsphere.datastore_cluster_overall_status | green, red, yellow, gray | status |
711 +
712 +### Per vSAN cluster
713 +
714 +These optional metrics refer to vSAN cluster capacity, health, and performance and are collected only when `collect_vsan` is enabled.
715 +
716 +Labels:
717 +
718 +| Label | Description |
719 +|:-----------|:----------------|
720 +| id | vSphere managed object reference ID of the cluster |
721 +| datacenter | Datacenter name |
722 +| cluster | Cluster name |
723 +| vsan_uuid | vSAN cluster UUID used by vSAN performance entity references |
724 +| vsphere_tag_<category> | vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character |
725 +| vsphere_custom_attribute_<name> | vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys |
726 +
727 +Metrics:
728 +
729 +| Metric | Dimensions | Unit |
730 +|:------|:----------|:----|
731 +| vsphere.vsan_cluster_space_usage | used, free, total | bytes |
732 +| vsphere.vsan_cluster_space_utilization | used | percentage |
733 +| vsphere.vsan_cluster_health_status | green, yellow, red, unknown | status |
734 +| vsphere.vsan_cluster_operations | read, write | operations/s |
735 +| vsphere.vsan_cluster_throughput | read, write | bytes/s |
736 +| vsphere.vsan_cluster_latency | read, write | microseconds |
737 +| vsphere.vsan_cluster_congestions | congestions | congestions/s |
738
739 ### Per cluster
740
@@ -407,8 +744,11 @@ Labels:
744
745 | Label | Description |
746 |:-----------|:----------------|
747 +| id | vSphere managed object reference ID |
748 | datacenter | Datacenter name |
749 | cluster | Cluster name |
750 +| vsphere_tag_<category> | vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character |
751 +| vsphere_custom_attribute_<name> | vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys |
752
753 Metrics:
754
@@ -419,7 +759,12 @@ Metrics:
759 | vsphere.cluster_mem_capacity | total, effective | bytes |
760 | vsphere.cluster_cpu_topology | cores, threads | count |
761 | vsphere.cluster_drs_config | enabled | status |
762 +| vsphere.cluster_drs_mode | manual, partially_automated, fully_automated, unknown | status |
763 +| vsphere.cluster_drs_vmotion_rate | rate | level |
764 | vsphere.cluster_ha_config | enabled, admission_control | status |
765 +| vsphere.cluster_ha_host_monitoring | enabled, disabled, unknown | status |
766 +| vsphere.cluster_ha_vm_monitoring | disabled, vm_monitoring_only, vm_and_app_monitoring, unknown | status |
767 +| vsphere.cluster_ha_vm_component_protection | enabled, disabled, unknown | status |
768 | vsphere.cluster_overall_status | green, red, yellow, gray | status |
769 | vsphere.cluster_vmotions | vmotions | migrations |
770 | vsphere.cluster_drs_score | score | percentage |
@@ -449,9 +794,12 @@ Labels:
794
795 | Label | Description |
796 |:-----------|:----------------|
797 +| id | vSphere managed object reference ID |
798 | datacenter | Datacenter name |
799 | cluster | Cluster name |
800 | resource_pool | Resource Pool name |
801 +| vsphere_tag_<category> | vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character |
802 +| vsphere_custom_attribute_<name> | vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys |
803
804 Metrics:
805
@@ -470,6 +818,90 @@ Metrics:
818
819
820
821 +## Live Data
822 +
823 +This collector exposes read-only readiness and topology functions for interactive troubleshooting in the Live tab. Both functions require selecting a configured vSphere collector job.
824 +
825 +
826 +### vSphere Readiness
827 +
828 +Reports the collector's current readiness from cached local state:
829 +
830 +- the selected vSphere collector job
831 +- whether the target URL and credentials are configured
832 +- whether the vSphere client, discovery cache, and performance-counter lists are initialized
833 +- discovered inventory counts
834 +- enabled or disabled optional metric, label, and vSAN groups
835 +- cached vSAN result counts when `collect_vsan` is enabled
836 +
837 +The function does not expose the configured vCenter URL or credentials, and it does not issue extra vCenter API calls.
838 +
839 +
840 +| Aspect | Description |
841 +|:-------|:------------|
842 +| Name | `Vsphere:readiness` |
843 +| Require Cloud | yes |
844 +| Performance | Uses cached collector state only:<br/>• No additional vCenter or ESXi API requests are triggered<br/>• Response size is bounded by the number of configured optional groups and cached inventory summary rows |
845 +| Security | Does not expose the configured vCenter URL, username, password, or per-object inventory names:<br/>• Shows only configuration presence, resource counts, enabled feature flags, include pattern counts, and cached vSAN result counts<br/>• Access should still be restricted to authorized operators because it reveals enabled collection surfaces |
846 +| Availability | Available when:<br/>• The vSphere collector job is running<br/>• Returns not_ready rows while the collector is not initialized or discovery has not completed<br/>• Uses the last cached discovery and vSAN scrape state |
847 +
848 +#### Prerequisites
849 +
850 +No additional configuration is required.
851 +
852 +#### Parameters
853 +
854 +| Parameter | Type | Description | Required | Default | Options |
855 +|:---------|:-----|:------------|:--------:|:--------|:--------|
856 +| Job | select | Select which configured vSphere collector job to inspect. | yes | | |
857 +
858 +#### Returns
859 +
860 +Collector readiness checks from cached local state. Each row represents one target, discovery, label, scope, metric, or vSAN readiness check.
861 +
862 +| Column | Type | Unit | Visibility | Description |
863 +|:-------|:-----|:-----|:-----------|:------------|
864 +| check | string | | | Stable readiness check identifier. |
865 +| scope | string | | | Area covered by the check, such as target, discovery, labels, scope, or metrics. |
866 +| status | string | | | Readiness status. Possible values are ok, warning, disabled, and not_ready. |
867 +| details | string | | | Human-readable explanation of the current cached state for the check. |
868 +
869 +### vSphere Topology
870 +
871 +Reports cached vSphere inventory topology for datacenters, clusters, ESXi hosts, VMs, datastores, networks, datastore clusters, and resource pools.
872 +
873 +The public topology function is `topology:vsphere`. The function builds actors and links from the selected job's cached discovery state. It does not issue extra vCenter API calls. vSphere Network and Distributed Virtual Port Group actors are included only when `collect_network_topology` is enabled.
874 +
875 +
876 +| Aspect | Description |
877 +|:-------|:------------|
878 +| Name | `Vsphere:topology:vsphere` |
879 +| Require Cloud | yes |
880 +| Performance | Uses cached collector state only:<br/>• No additional vCenter or ESXi API requests are triggered by the function<br/>• Response size grows with discovered inventory object count<br/>• `collect_network_topology` adds Network discovery during normal collector discovery cycles when enabled |
881 +| Security | Exposes discovered inventory object names and status attributes already visible through vSphere chart labels and metrics:<br/>• Does not expose the configured vCenter URL, username, or password |
882 +| Availability | Available when:<br/>• The vSphere collector job is running<br/>• Initial discovery has completed successfully<br/>• Returns HTTP 503 while topology data is not cached yet |
883 +
884 +#### Prerequisites
885 +
886 +No additional configuration is required.
887 +
888 +#### Parameters
889 +
890 +| Parameter | Type | Description | Required | Default | Options |
891 +|:---------|:-----|:------------|:--------:|:--------|:--------|
892 +| Job | select | Select which configured vSphere collector job provides the cached topology. | yes | | |
893 +
894 +#### Returns
895 +
896 +Cached vSphere inventory topology. Actors represent discovered inventory objects and links represent parent-child, host-runs-VM, or host/VM-connects-network relationships.
897 +
898 +| Column | Type | Unit | Visibility | Description |
899 +|:-------|:-----|:-----|:-----------|:------------|
900 +| actors | array | | | vSphere inventory actors, including datacenters, clusters, ESXi hosts, VMs, datastores, optional networks, datastore clusters, and resource pools. |
901 +| links | array | | | Topology links between vSphere inventory actors. |
902 +
903 +
904 +
905 ## Troubleshooting
906
907 ### Debug Mode
src/go/plugin/go.d/collector/vsphere/labels.go new
+53
@@ -0,0 +1,53 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import (
6 + "sort"
7 + "strings"
8 +
9 + "github.com/netdata/netdata/go/plugins/pkg/metrix"
10 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
11 +)
12 +
13 +func resourceEnrichmentLabels(labels map[string]string) []metrix.Label {
14 + if len(labels) == 0 {
15 + return nil
16 + }
17 +
18 + keys := make([]string, 0, len(labels))
19 + for key, value := range labels {
20 + if key != "" && value != "" {
21 + keys = append(keys, key)
22 + }
23 + }
24 + if len(keys) == 0 {
25 + return nil
26 + }
27 +
28 + sort.Strings(keys)
29 +
30 + out := make([]metrix.Label, 0, len(keys))
31 + for _, key := range keys {
32 + out = append(out, metrix.Label{Key: key, Value: labels[key]})
33 + }
34 + return out
35 +}
36 +
37 +func getVMClusterName(vm *rs.VM) string {
38 + if isStandaloneHostClusterID(vm.Hier.Cluster.ID) {
39 + return ""
40 + }
41 + return vm.Hier.Cluster.Name
42 +}
43 +
44 +func getHostClusterName(host *rs.Host) string {
45 + if isStandaloneHostClusterID(host.Hier.Cluster.ID) {
46 + return ""
47 + }
48 + return host.Hier.Cluster.Name
49 +}
50 +
51 +func isStandaloneHostClusterID(id string) bool {
52 + return strings.HasPrefix(id, "domain-s")
53 +}
src/go/plugin/go.d/collector/vsphere/labels_test.go new
+135
@@ -0,0 +1,135 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import (
6 + "context"
7 + "testing"
8 +
9 + "github.com/stretchr/testify/require"
10 +
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/match"
12 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
13 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
14 +)
15 +
16 +func TestCollector_AddsUserMetadataLabels(t *testing.T) {
17 + collr, _, teardown := prepareVSphereSim(t)
18 + defer teardown()
19 +
20 + require.NoError(t, collr.Init(context.Background()))
21 + collr.scraper = mockScraper{collr.scraper}
22 + for _, vm := range collr.resources.VMs {
23 + vm.Labels = map[string]string{
24 + "vsphere_custom_attribute_owner": "platform",
25 + "vsphere_tag_service": "payments",
26 + }
27 + }
28 + for _, host := range collr.resources.Hosts {
29 + host.Labels = map[string]string{"vsphere_tag_env": "prod"}
30 + }
31 +
32 + require.NotEmpty(t, collectScalarSeriesForTest(t, collr))
33 +
34 + vm := firstSortedVM(t, collr)
35 + host := firstSortedHost(t, collr)
36 + createdCharts, _ := v2CreatedChartsAndDims(buildV2PlanForTest(t, collr))
37 +
38 + vmChartID := findChartIDByLabelsAndContext(t, createdCharts, "vsphere.vm_cpu_utilization", map[string]string{"id": vm.ID})
39 + require.Equal(t, "platform", createdCharts[vmChartID].Labels["vsphere_custom_attribute_owner"])
40 + require.Equal(t, "payments", createdCharts[vmChartID].Labels["vsphere_tag_service"])
41 +
42 + hostChartID := findChartIDByLabelsAndContext(t, createdCharts, "vsphere.host_cpu_utilization", map[string]string{"id": host.ID})
43 + require.Equal(t, "prod", createdCharts[hostChartID].Labels["vsphere_tag_env"])
44 + collecttest.AssertChartCoverage(t, collr, collecttest.ChartCoverageExpectation{})
45 +}
46 +
47 +func TestClusterNameLabels(t *testing.T) {
48 + tests := map[string]struct {
49 + host *rs.Host
50 + vm *rs.VM
51 + want string
52 + }{
53 + "standalone host dummy cluster": {
54 + host: &rs.Host{
55 + Name: "Host1",
56 + Hier: rs.HostHierarchy{Cluster: rs.HierarchyValue{
57 + ID: "domain-s1",
58 + Name: "Host1",
59 + }},
60 + },
61 + vm: &rs.VM{
62 + Hier: rs.VMHierarchy{
63 + Cluster: rs.HierarchyValue{ID: "domain-s1", Name: "Host1"},
64 + Host: rs.HierarchyValue{Name: "Host1"},
65 + },
66 + },
67 + want: "",
68 + },
69 + "real cluster with same name as host": {
70 + host: &rs.Host{
71 + Name: "Host1",
72 + Hier: rs.HostHierarchy{Cluster: rs.HierarchyValue{
73 + ID: "domain-c1",
74 + Name: "Host1",
75 + }},
76 + },
77 + vm: &rs.VM{
78 + Hier: rs.VMHierarchy{
79 + Cluster: rs.HierarchyValue{ID: "domain-c1", Name: "Host1"},
80 + Host: rs.HierarchyValue{Name: "Host1"},
81 + },
82 + },
83 + want: "Host1",
84 + },
85 + }
86 +
87 + for name, tc := range tests {
88 + t.Run(name, func(t *testing.T) {
89 + require.Equal(t, tc.want, getHostClusterName(tc.host))
90 + require.Equal(t, tc.want, getVMClusterName(tc.vm))
91 + })
92 + }
93 +}
94 +
95 +func TestCollector_Init_ReturnsFalseIfInvalidUserMetadataLabelConfig(t *testing.T) {
96 + tests := map[string]struct {
97 + setup func(*Collector)
98 + want string
99 + }{
100 + "invalid tag category pattern": {
101 + setup: func(c *Collector) { c.TagCategories = []string{"["} },
102 + want: "tag_categories has invalid pattern",
103 + },
104 + "empty negative tag category pattern": {
105 + setup: func(c *Collector) { c.TagCategories = []string{"!"} },
106 + want: "tag_categories has invalid empty negative pattern",
107 + },
108 + "all-negative custom attribute pattern list": {
109 + setup: func(c *Collector) { c.CustomAttributes = []string{"!Secret"} },
110 + want: "custom_attributes must include at least one positive pattern",
111 + },
112 + }
113 +
114 + for name, tc := range tests {
115 + t.Run(name, func(t *testing.T) {
116 + collr := New()
117 + collr.URL = "https://vcenter.local"
118 + collr.Username = "user"
119 + collr.Password = "pass"
120 + tc.setup(collr)
121 +
122 + require.ErrorContains(t, collr.Init(context.Background()), tc.want)
123 + })
124 + }
125 +}
126 +
127 +func TestPatternListMatcherPreservesUserMetadataListItems(t *testing.T) {
128 + m, err := match.NewPatternListMatcher("custom_attributes", []string{"!Business Secret", "Cost Center", "Business*"})
129 + require.NoError(t, err)
130 +
131 + require.True(t, m.MatchString("Cost Center"))
132 + require.True(t, m.MatchString("Business Unit"))
133 + require.False(t, m.MatchString("Cost"))
134 + require.False(t, m.MatchString("Business Secret"))
135 +}
src/go/plugin/go.d/collector/vsphere/match/match.go
+204 -290
@@ -26,80 +26,72 @@ type ClusterMatcher interface {
26 Match(*rs.Cluster) bool
27 }
28
29 +type DatastoreClusterMatcher interface {
30 + Match(*rs.StoragePod) bool
31 +}
32 +
33 +type VSANClusterMatcher interface {
34 + Match(*rs.Cluster) bool
35 +}
36 +
37 +type VSANHostMatcher interface {
38 + Match(*rs.Host) bool
39 +}
40 +
41 +type VSANVMMatcher interface {
42 + Match(*rs.VM) bool
43 +}
44 +
45 type (
30 - hostDCMatcher struct{ m matcher.Matcher }
31 - hostClusterMatcher struct{ m matcher.Matcher }
32 - hostHostMatcher struct{ m matcher.Matcher }
33 - vmDCMatcher struct{ m matcher.Matcher }
34 - vmClusterMatcher struct{ m matcher.Matcher }
35 - vmHostMatcher struct{ m matcher.Matcher }
36 - vmVMMatcher struct{ m matcher.Matcher }
37 - orHostMatcher struct{ lhs, rhs HostMatcher }
38 - orVMMatcher struct{ lhs, rhs VMMatcher }
39 - andHostMatcher struct{ lhs, rhs HostMatcher }
40 - andVMMatcher struct{ lhs, rhs VMMatcher }
46 + resourceMatcher[T any] interface {
47 + Match(*T) bool
48 + }
49 + fieldMatcher[T any] struct {
50 + m matcher.Matcher
51 + get func(*T) string
52 + }
53 + orMatcher[T any] struct{ lhs, rhs resourceMatcher[T] }
54 + andMatcher[T any] struct{ lhs, rhs resourceMatcher[T] }
55 )
56
43 -func (m hostDCMatcher) Match(host *rs.Host) bool { return m.m.MatchString(host.Hier.DC.Name) }
44 -func (m hostClusterMatcher) Match(host *rs.Host) bool { return m.m.MatchString(host.Hier.Cluster.Name) }
45 -func (m hostHostMatcher) Match(host *rs.Host) bool { return m.m.MatchString(host.Name) }
46 -func (m vmDCMatcher) Match(vm *rs.VM) bool { return m.m.MatchString(vm.Hier.DC.Name) }
47 -func (m vmClusterMatcher) Match(vm *rs.VM) bool { return m.m.MatchString(vm.Hier.Cluster.Name) }
48 -func (m vmHostMatcher) Match(vm *rs.VM) bool { return m.m.MatchString(vm.Hier.Host.Name) }
49 -func (m vmVMMatcher) Match(vm *rs.VM) bool { return m.m.MatchString(vm.Name) }
50 -func (m orHostMatcher) Match(host *rs.Host) bool { return m.lhs.Match(host) || m.rhs.Match(host) }
51 -func (m orVMMatcher) Match(vm *rs.VM) bool { return m.lhs.Match(vm) || m.rhs.Match(vm) }
52 -func (m andHostMatcher) Match(host *rs.Host) bool { return m.lhs.Match(host) && m.rhs.Match(host) }
53 -func (m andVMMatcher) Match(vm *rs.VM) bool { return m.lhs.Match(vm) && m.rhs.Match(vm) }
54 -
55 -func newAndHostMatcher(lhs, rhs HostMatcher, others ...HostMatcher) andHostMatcher {
56 - m := andHostMatcher{lhs: lhs, rhs: rhs}
57 - switch len(others) {
58 - case 0:
59 - return m
60 - default:
61 - return newAndHostMatcher(m, others[0], others[1:]...)
62 - }
57 +func (m fieldMatcher[T]) Match(v *T) bool {
58 + return m.m.MatchString(m.get(v))
59 }
60
65 -func newAndVMMatcher(lhs, rhs VMMatcher, others ...VMMatcher) andVMMatcher {
66 - m := andVMMatcher{lhs: lhs, rhs: rhs}
67 - switch len(others) {
68 - case 0:
69 - return m
70 - default:
71 - return newAndVMMatcher(m, others[0], others[1:]...)
72 - }
61 +func (m orMatcher[T]) Match(v *T) bool {
62 + return m.lhs.Match(v) || m.rhs.Match(v)
63 }
64
75 -func newOrHostMatcher(lhs, rhs HostMatcher, others ...HostMatcher) orHostMatcher {
76 - m := orHostMatcher{lhs: lhs, rhs: rhs}
77 - switch len(others) {
78 - case 0:
79 - return m
80 - default:
81 - return newOrHostMatcher(m, others[0], others[1:]...)
82 - }
65 +func (m andMatcher[T]) Match(v *T) bool {
66 + return m.lhs.Match(v) && m.rhs.Match(v)
67 }
68
85 -func newOrVMMatcher(lhs, rhs VMMatcher, others ...VMMatcher) orVMMatcher {
86 - m := orVMMatcher{lhs: lhs, rhs: rhs}
87 - switch len(others) {
69 +func chainAnd[T any](ms []resourceMatcher[T]) resourceMatcher[T] {
70 + switch len(ms) {
71 case 0:
89 - return m
90 - default:
91 - return newOrVMMatcher(m, others[0], others[1:]...)
72 + return nil
73 + case 1:
74 + return ms[0]
75 + }
76 + m := andMatcher[T]{lhs: ms[0], rhs: ms[1]}
77 + for _, next := range ms[2:] {
78 + m = andMatcher[T]{lhs: m, rhs: next}
79 }
80 + return m
81 }
82
95 -func newOrDSMatcher(lhs, rhs DatastoreMatcher, others ...DatastoreMatcher) orDSMatcher {
96 - m := orDSMatcher{lhs: lhs, rhs: rhs}
97 - switch len(others) {
83 +func chainOr[T any](ms []resourceMatcher[T]) resourceMatcher[T] {
84 + switch len(ms) {
85 case 0:
99 - return m
100 - default:
101 - return newOrDSMatcher(m, others[0], others[1:]...)
86 + return nil
87 + case 1:
88 + return ms[0]
89 + }
90 + m := orMatcher[T]{lhs: ms[0], rhs: ms[1]}
91 + for _, next := range ms[2:] {
92 + m = orMatcher[T]{lhs: m, rhs: next}
93 }
94 + return m
95 }
96
97 type (
@@ -107,303 +99,225 @@ type (
99 HostIncludes []string
100 DatastoreIncludes []string
101 ClusterIncludes []string
110 -)
102
112 -type (
113 - dsDCMatcher struct{ m matcher.Matcher }
114 - dsDSMatcher struct{ m matcher.Matcher }
115 - orDSMatcher struct{ lhs, rhs DatastoreMatcher }
116 - andDSMatcher struct{ lhs, rhs DatastoreMatcher }
103 + DatastoreClusterIncludes []string
104 + VSANClusterIncludes []string
105 + VSANHostIncludes []string
106 + VSANVMIncludes []string
107 )
108
119 -func (m dsDCMatcher) Match(ds *rs.Datastore) bool { return m.m.MatchString(ds.Hier.DC.Name) }
120 -func (m dsDSMatcher) Match(ds *rs.Datastore) bool { return m.m.MatchString(ds.Name) }
121 -func (m orDSMatcher) Match(ds *rs.Datastore) bool { return m.lhs.Match(ds) || m.rhs.Match(ds) }
122 -func (m andDSMatcher) Match(ds *rs.Datastore) bool { return m.lhs.Match(ds) && m.rhs.Match(ds) }
123 -
109 type (
125 - clusterDCMatcher struct{ m matcher.Matcher }
126 - clusterNameMatcher struct{ m matcher.Matcher }
127 - orClusterMatcher struct{ lhs, rhs ClusterMatcher }
128 - andClusterMatcher struct{ lhs, rhs ClusterMatcher }
110 + datastoreClusterMatcher struct{ m matcher.Matcher }
111 + vsanClusterMatcher struct{ m matcher.Matcher }
112 + vsanHostMatcher struct{ m matcher.Matcher }
113 + vsanVMMatcher struct{ m matcher.Matcher }
114 )
115
131 -func (m clusterDCMatcher) Match(c *rs.Cluster) bool { return m.m.MatchString(c.Hier.DC.Name) }
132 -func (m clusterNameMatcher) Match(c *rs.Cluster) bool { return m.m.MatchString(c.Name) }
133 -func (m orClusterMatcher) Match(c *rs.Cluster) bool { return m.lhs.Match(c) || m.rhs.Match(c) }
134 -func (m andClusterMatcher) Match(c *rs.Cluster) bool { return m.lhs.Match(c) && m.rhs.Match(c) }
135 -
136 -func newOrClusterMatcher(lhs, rhs ClusterMatcher, others ...ClusterMatcher) orClusterMatcher {
137 - m := orClusterMatcher{lhs: lhs, rhs: rhs}
138 - switch len(others) {
139 - case 0:
140 - return m
141 - default:
142 - return newOrClusterMatcher(m, others[0], others[1:]...)
116 +// NewPatternListMatcher parses ordered glob patterns with optional !-prefixed exclusions.
117 +func NewPatternListMatcher(name string, patterns []string) (matcher.Matcher, error) {
118 + m, err := matcher.NewSimplePatternListMatcher(patterns)
119 + if err != nil {
120 + return nil, patternListError(name, err)
121 }
122 + return m, nil
123 }
124
146 -func (vi VMIncludes) Parse() (VMMatcher, error) {
147 - var ms []VMMatcher
148 - for _, v := range vi {
149 - m, err := parseVMInclude(v)
150 - if err != nil {
151 - return nil, err
152 - }
153 - if m == nil {
154 - continue
155 - }
156 - ms = append(ms, m)
125 +func patternListError(name string, err error) error {
126 + if err.Error() == "invalid empty negative pattern" || strings.HasPrefix(err.Error(), "invalid pattern") {
127 + return fmt.Errorf("%s has %w", name, err)
128 }
129 + return fmt.Errorf("%s %w", name, err)
130 +}
131
159 - switch len(ms) {
160 - case 0:
161 - return nil, nil
162 - case 1:
163 - return ms[0], nil
164 - default:
165 - return newOrVMMatcher(ms[0], ms[1], ms[2:]...), nil
166 - }
132 +func (m datastoreClusterMatcher) Match(pod *rs.StoragePod) bool {
133 + return m.m.MatchString(datastoreClusterPath(pod)) ||
134 + m.m.MatchString(pod.Name) ||
135 + m.m.MatchString(pod.ID)
136 }
137
169 -func (hi HostIncludes) Parse() (HostMatcher, error) {
170 - var ms []HostMatcher
171 - for _, v := range hi {
172 - m, err := parseHostInclude(v)
173 - if err != nil {
174 - return nil, err
175 - }
176 - if m == nil {
177 - continue
178 - }
179 - ms = append(ms, m)
180 - }
138 +func (m vsanClusterMatcher) Match(cluster *rs.Cluster) bool {
139 + return m.m.MatchString(vsanClusterPath(cluster)) ||
140 + m.m.MatchString(cluster.Name) ||
141 + m.m.MatchString(cluster.ID) ||
142 + m.m.MatchString("vsan_uuid:"+cluster.VSANUUID)
143 +}
144
182 - switch len(ms) {
183 - case 0:
184 - return nil, nil
185 - case 1:
186 - return ms[0], nil
187 - default:
188 - return newOrHostMatcher(ms[0], ms[1], ms[2:]...), nil
189 - }
145 +func (m vsanHostMatcher) Match(host *rs.Host) bool {
146 + return m.m.MatchString(vsanHostPath(host)) ||
147 + m.m.MatchString(host.Name) ||
148 + m.m.MatchString(host.ID) ||
149 + m.m.MatchString("vsan_node_uuid:"+host.VSANNodeUUID)
150 }
151
192 -func (di DatastoreIncludes) Parse() (DatastoreMatcher, error) {
193 - var ms []DatastoreMatcher
194 - for _, v := range di {
195 - m, err := parseDatastoreInclude(v)
196 - if err != nil {
197 - return nil, err
198 - }
199 - if m == nil {
200 - continue
201 - }
202 - ms = append(ms, m)
152 +func (m vsanVMMatcher) Match(vm *rs.VM) bool {
153 + return m.m.MatchString(vsanVMPath(vm)) ||
154 + m.m.MatchString(vm.Name) ||
155 + m.m.MatchString(vm.ID) ||
156 + m.m.MatchString("instance_uuid:"+vm.InstanceUUID)
157 +}
158 +
159 +func datastoreClusterPath(pod *rs.StoragePod) string {
160 + if pod.Hier.DC.Name == "" {
161 + return "/" + pod.Name
162 }
163 + return "/" + pod.Hier.DC.Name + "/" + pod.Name
164 +}
165
205 - switch len(ms) {
206 - case 0:
207 - return nil, nil
208 - case 1:
209 - return ms[0], nil
210 - default:
211 - return newOrDSMatcher(ms[0], ms[1], ms[2:]...), nil
166 +func vsanClusterPath(cluster *rs.Cluster) string {
167 + if cluster.Hier.DC.Name == "" {
168 + return "/" + cluster.Name
169 }
170 + return "/" + cluster.Hier.DC.Name + "/" + cluster.Name
171 }
172
215 -const (
216 - datacenterIdx = iota
217 - clusterIdx
218 - hostIdx
219 - vmIdx
220 -)
173 +func vsanHostPath(host *rs.Host) string {
174 + return "/" + host.Hier.DC.Name + "/" + host.Hier.Cluster.Name + "/" + host.Name
175 +}
176
222 -func cleanInclude(include string) string {
223 - return strings.Trim(include, "/")
177 +func vsanVMPath(vm *rs.VM) string {
178 + return "/" + vm.Hier.DC.Name + "/" + vm.Hier.Cluster.Name + "/" + vm.Hier.Host.Name + "/" + vm.Name
179 }
180
226 -func parseHostInclude(include string) (HostMatcher, error) {
227 - if !isIncludeFormatValid(include) {
228 - return nil, fmt.Errorf("bad include format: %s", include)
181 +func (dci DatastoreClusterIncludes) Parse() (DatastoreClusterMatcher, error) {
182 + m, err := NewPatternListMatcher("datastore_cluster_include", []string(dci))
183 + if err != nil {
184 + return nil, err
185 }
186 + return datastoreClusterMatcher{m}, nil
187 +}
188
231 - include = cleanInclude(include)
232 - parts := strings.Split(include, "/") // /dc/clusterIdx/hostIdx
233 - var ms []HostMatcher
234 -
235 - for i, v := range parts {
236 - m, err := parseSubInclude(v)
237 - if err != nil {
238 - return nil, err
239 - }
240 - switch i {
241 - case datacenterIdx:
242 - ms = append(ms, hostDCMatcher{m})
243 - case clusterIdx:
244 - ms = append(ms, hostClusterMatcher{m})
245 - case hostIdx:
246 - ms = append(ms, hostHostMatcher{m})
247 - default:
248 - }
189 +func (vci VSANClusterIncludes) Parse() (VSANClusterMatcher, error) {
190 + m, err := NewPatternListMatcher("vsan_cluster_include", []string(vci))
191 + if err != nil {
192 + return nil, err
193 }
194 + return vsanClusterMatcher{m}, nil
195 +}
196
251 - switch len(ms) {
252 - case 0:
253 - return nil, nil
254 - case 1:
255 - return ms[0], nil
256 - default:
257 - return newAndHostMatcher(ms[0], ms[1], ms[2:]...), nil
197 +func (vhi VSANHostIncludes) Parse() (VSANHostMatcher, error) {
198 + m, err := NewPatternListMatcher("vsan_host_include", []string(vhi))
199 + if err != nil {
200 + return nil, err
201 }
202 + return vsanHostMatcher{m}, nil
203 }
204
261 -func parseVMInclude(include string) (VMMatcher, error) {
262 - if !isIncludeFormatValid(include) {
263 - return nil, fmt.Errorf("bad include format: %s", include)
205 +func (vvi VSANVMIncludes) Parse() (VSANVMMatcher, error) {
206 + m, err := NewPatternListMatcher("vsan_vm_include", []string(vvi))
207 + if err != nil {
208 + return nil, err
209 }
210 + return vsanVMMatcher{m}, nil
211 +}
212
266 - include = cleanInclude(include)
267 - parts := strings.Split(include, "/") // /dc/clusterIdx/hostIdx/vmIdx
268 - var ms []VMMatcher
269 -
270 - for i, v := range parts {
271 - m, err := parseSubInclude(v)
272 - if err != nil {
273 - return nil, err
274 - }
275 - switch i {
276 - case datacenterIdx:
277 - ms = append(ms, vmDCMatcher{m})
278 - case clusterIdx:
279 - ms = append(ms, vmClusterMatcher{m})
280 - case hostIdx:
281 - ms = append(ms, vmHostMatcher{m})
282 - case vmIdx:
283 - ms = append(ms, vmVMMatcher{m})
284 - }
285 - }
213 +func (vi VMIncludes) Parse() (VMMatcher, error) {
214 + return parseIncludes[rs.VM]("VM include", []string(vi), parseVMInclude)
215 +}
216
287 - switch len(ms) {
288 - case 0:
289 - return nil, nil
290 - case 1:
291 - return ms[0], nil
292 - default:
293 - return newAndVMMatcher(ms[0], ms[1], ms[2:]...), nil
294 - }
217 +func (hi HostIncludes) Parse() (HostMatcher, error) {
218 + return parseIncludes[rs.Host]("host include", []string(hi), parseHostInclude)
219 }
220
297 -func parseSubInclude(sub string) (matcher.Matcher, error) {
298 - sub = strings.TrimSpace(sub)
299 - if sub == "" || sub == "!*" {
300 - return matcher.FALSE(), nil
301 - }
302 - if sub == "*" {
303 - return matcher.TRUE(), nil
304 - }
305 - return matcher.NewSimplePatternsMatcher(sub)
221 +func (di DatastoreIncludes) Parse() (DatastoreMatcher, error) {
222 + return parseIncludes[rs.Datastore]("datastore include", []string(di), parseDatastoreInclude)
223 }
224
308 -func isIncludeFormatValid(line string) bool {
309 - return strings.HasPrefix(line, "/")
225 +func cleanInclude(include string) string {
226 + return strings.Trim(include, "/")
227 }
228
312 -func (ci ClusterIncludes) Parse() (ClusterMatcher, error) {
313 - var ms []ClusterMatcher
314 - for _, v := range ci {
315 - m, err := parseClusterInclude(v)
229 +func parseIncludes[T any](name string, includes []string, parse func(string) (resourceMatcher[T], error)) (resourceMatcher[T], error) {
230 + var ms []resourceMatcher[T]
231 + for _, v := range includes {
232 + m, err := parse(v)
233 if err != nil {
317 - return nil, err
234 + return nil, fmt.Errorf("parse %s %q: %w", name, v, err)
235 }
236 if m == nil {
237 continue
238 }
239 ms = append(ms, m)
240 }
324 -
325 - switch len(ms) {
326 - case 0:
327 - return nil, nil
328 - case 1:
329 - return ms[0], nil
330 - default:
331 - return newOrClusterMatcher(ms[0], ms[1], ms[2:]...), nil
332 - }
241 + return chainOr(ms), nil
242 }
243
335 -const (
336 - clDCIdx = iota
337 - clClusterIdx
338 -)
339 -
340 -func parseClusterInclude(include string) (ClusterMatcher, error) {
244 +func parsePathInclude[T any](include, name, expected string, segments []func(*T) string) (resourceMatcher[T], error) {
245 if !isIncludeFormatValid(include) {
342 - return nil, fmt.Errorf("bad include format: %s", include)
246 + return nil, fmt.Errorf("bad %s include format %q: expected %s", name, include, expected)
247 }
248
249 include = cleanInclude(include)
346 - parts := strings.Split(include, "/") // /dc/cluster
347 - var ms []ClusterMatcher
250 + parts := strings.Split(include, "/")
251 + ms := make([]resourceMatcher[T], 0, min(len(parts), len(segments)))
252
253 for i, v := range parts {
254 m, err := parseSubInclude(v)
255 if err != nil {
352 - return nil, err
256 + return nil, fmt.Errorf("parse %s include segment index=%d value=%q: %w", name, i, v, err)
257 }
354 - switch i {
355 - case clDCIdx:
356 - ms = append(ms, clusterDCMatcher{m})
357 - case clClusterIdx:
358 - ms = append(ms, clusterNameMatcher{m})
359 - default:
258 + if i >= len(segments) {
259 + continue
260 }
261 + ms = append(ms, fieldMatcher[T]{m: m, get: segments[i]})
262 }
263
363 - switch len(ms) {
364 - case 0:
365 - return nil, nil
366 - case 1:
367 - return ms[0], nil
368 - default:
369 - return andClusterMatcher{lhs: ms[0], rhs: ms[1]}, nil
370 - }
264 + return chainAnd(ms), nil
265 }
266
373 -const (
374 - dsDatacenterIdx = iota
375 - dsDatastoreIdx
376 -)
267 +var hostPathSegments = []func(*rs.Host) string{
268 + func(host *rs.Host) string { return host.Hier.DC.Name },
269 + func(host *rs.Host) string { return host.Hier.Cluster.Name },
270 + func(host *rs.Host) string { return host.Name },
271 +}
272
378 -func parseDatastoreInclude(include string) (DatastoreMatcher, error) {
379 - if !isIncludeFormatValid(include) {
380 - return nil, fmt.Errorf("bad include format: %s", include)
381 - }
273 +var vmPathSegments = []func(*rs.VM) string{
274 + func(vm *rs.VM) string { return vm.Hier.DC.Name },
275 + func(vm *rs.VM) string { return vm.Hier.Cluster.Name },
276 + func(vm *rs.VM) string { return vm.Hier.Host.Name },
277 + func(vm *rs.VM) string { return vm.Name },
278 +}
279
383 - include = cleanInclude(include)
384 - parts := strings.Split(include, "/") // /dc/datastore
385 - var ms []DatastoreMatcher
280 +var clusterPathSegments = []func(*rs.Cluster) string{
281 + func(cluster *rs.Cluster) string { return cluster.Hier.DC.Name },
282 + func(cluster *rs.Cluster) string { return cluster.Name },
283 +}
284
387 - for i, v := range parts {
388 - m, err := parseSubInclude(v)
389 - if err != nil {
390 - return nil, err
391 - }
392 - switch i {
393 - case dsDatacenterIdx:
394 - ms = append(ms, dsDCMatcher{m})
395 - case dsDatastoreIdx:
396 - ms = append(ms, dsDSMatcher{m})
397 - default:
398 - }
399 - }
285 +var datastorePathSegments = []func(*rs.Datastore) string{
286 + func(ds *rs.Datastore) string { return ds.Hier.DC.Name },
287 + func(ds *rs.Datastore) string { return ds.Name },
288 +}
289
401 - switch len(ms) {
402 - case 0:
403 - return nil, nil
404 - case 1:
405 - return ms[0], nil
406 - default:
407 - return andDSMatcher{lhs: ms[0], rhs: ms[1]}, nil
290 +func parseHostInclude(include string) (resourceMatcher[rs.Host], error) {
291 + return parsePathInclude(include, "host", "/<datacenter>/<cluster>/<host>", hostPathSegments)
292 +}
293 +
294 +func parseVMInclude(include string) (resourceMatcher[rs.VM], error) {
295 + return parsePathInclude(include, "VM", "/<datacenter>/<cluster>/<host>/<vm>", vmPathSegments)
296 +}
297 +
298 +func parseClusterInclude(include string) (resourceMatcher[rs.Cluster], error) {
299 + return parsePathInclude(include, "cluster", "/<datacenter>/<cluster>", clusterPathSegments)
300 +}
301 +
302 +func parseDatastoreInclude(include string) (resourceMatcher[rs.Datastore], error) {
303 + return parsePathInclude(include, "datastore", "/<datacenter>/<datastore>", datastorePathSegments)
304 +}
305 +
306 +func parseSubInclude(sub string) (matcher.Matcher, error) {
307 + sub = strings.TrimSpace(sub)
308 + if sub == "" || sub == "!*" {
309 + return matcher.FALSE(), nil
310 }
311 + if sub == "*" {
312 + return matcher.TRUE(), nil
313 + }
314 + return matcher.NewSimplePatternsMatcher(sub)
315 +}
316 +
317 +func isIncludeFormatValid(line string) bool {
318 + return strings.HasPrefix(line, "/")
319 +}
320 +
321 +func (ci ClusterIncludes) Parse() (ClusterMatcher, error) {
322 + return parseIncludes[rs.Cluster]("cluster include", []string(ci), parseClusterInclude)
323 }
src/go/plugin/go.d/collector/vsphere/match/match_test.go
+542 -326
@@ -6,173 +6,132 @@ import (
6 "strings"
7 "testing"
8
9 - "github.com/netdata/netdata/go/plugins/pkg/matcher"
9 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
10
11 "github.com/stretchr/testify/assert"
12 )
13
15 -var (
16 - trueHostDC = hostDCMatcher{matcher.TRUE()}
17 - falseHostDC = hostDCMatcher{matcher.FALSE()}
18 - trueVMDC = vmDCMatcher{matcher.TRUE()}
19 - falseVMDC = vmDCMatcher{matcher.FALSE()}
20 - trueDSDC = dsDCMatcher{matcher.TRUE()}
21 - falseDSDC = dsDCMatcher{matcher.FALSE()}
22 - trueClDC = clusterDCMatcher{matcher.TRUE()}
23 - falseClDC = clusterDCMatcher{matcher.FALSE()}
24 -)
25 -
26 -func TestOrHostMatcher_Match(t *testing.T) {
27 - tests := map[string]struct {
28 - expected bool
29 - lhs HostMatcher
30 - rhs HostMatcher
31 - }{
32 - "true, true": {expected: true, lhs: trueHostDC, rhs: trueHostDC},
33 - "true, false": {expected: true, lhs: trueHostDC, rhs: falseHostDC},
34 - "false, true": {expected: true, lhs: falseHostDC, rhs: trueHostDC},
35 - "false, false": {expected: false, lhs: falseHostDC, rhs: falseHostDC},
36 - }
37 -
38 - var host resources.Host
39 - for name, test := range tests {
40 - t.Run(name, func(t *testing.T) {
41 - m := newOrHostMatcher(test.lhs, test.rhs)
42 - assert.Equal(t, test.expected, m.Match(&host))
43 - })
44 - }
45 -}
46 -
47 -func TestAndHostMatcher_Match(t *testing.T) {
48 - tests := map[string]struct {
49 - expected bool
50 - lhs HostMatcher
51 - rhs HostMatcher
52 - }{
53 - "true, true": {expected: true, lhs: trueHostDC, rhs: trueHostDC},
54 - "true, false": {expected: false, lhs: trueHostDC, rhs: falseHostDC},
55 - "false, true": {expected: false, lhs: falseHostDC, rhs: trueHostDC},
56 - "false, false": {expected: false, lhs: falseHostDC, rhs: falseHostDC},
57 - }
58 -
59 - var host resources.Host
60 - for name, test := range tests {
61 - t.Run(name, func(t *testing.T) {
62 - m := newAndHostMatcher(test.lhs, test.rhs)
63 - assert.Equal(t, test.expected, m.Match(&host))
64 - })
65 - }
66 -}
67 -
68 -func TestOrVMMatcher_Match(t *testing.T) {
69 - tests := map[string]struct {
70 - expected bool
71 - lhs VMMatcher
72 - rhs VMMatcher
73 - }{
74 - "true, true": {expected: true, lhs: trueVMDC, rhs: trueVMDC},
75 - "true, false": {expected: true, lhs: trueVMDC, rhs: falseVMDC},
76 - "false, true": {expected: true, lhs: falseVMDC, rhs: trueVMDC},
77 - "false, false": {expected: false, lhs: falseVMDC, rhs: falseVMDC},
78 - }
79 -
80 - var vm resources.VM
81 - for name, test := range tests {
82 - t.Run(name, func(t *testing.T) {
83 - m := newOrVMMatcher(test.lhs, test.rhs)
84 - assert.Equal(t, test.expected, m.Match(&vm))
85 - })
86 - }
87 -}
88 -
89 -func TestAndVMMatcher_Match(t *testing.T) {
90 - tests := map[string]struct {
91 - expected bool
92 - lhs VMMatcher
93 - rhs VMMatcher
94 - }{
95 - "true, true": {expected: true, lhs: trueVMDC, rhs: trueVMDC},
96 - "true, false": {expected: false, lhs: trueVMDC, rhs: falseVMDC},
97 - "false, true": {expected: false, lhs: falseVMDC, rhs: trueVMDC},
98 - "false, false": {expected: false, lhs: falseVMDC, rhs: falseVMDC},
99 - }
100 -
101 - var vm resources.VM
102 - for name, test := range tests {
103 - t.Run(name, func(t *testing.T) {
104 - m := newAndVMMatcher(test.lhs, test.rhs)
105 - assert.Equal(t, test.expected, m.Match(&vm))
106 - })
107 - }
108 -}
109 -
14 func TestHostIncludes_Parse(t *testing.T) {
15 tests := map[string]struct {
112 - valid bool
113 - expected HostMatcher
16 + valid bool
17 + cases map[string]struct {
18 + host *resources.Host
19 + want bool
20 + }
21 }{
22 "": {valid: false},
23 "*/C1/H1": {valid: false},
117 - "/": {valid: true, expected: falseHostDC},
118 - "/*": {valid: true, expected: trueHostDC},
119 - "/!*": {valid: true, expected: falseHostDC},
120 - "/!*/": {valid: true, expected: falseHostDC},
24 + "/": {
25 + valid: true,
26 + cases: map[string]struct {
27 + host *resources.Host
28 + want bool
29 + }{
30 + "does not match": {host: testHost("DC1", "Cluster1", "Host1"), want: false},
31 + },
32 + },
33 + "/*": {
34 + valid: true,
35 + cases: map[string]struct {
36 + host *resources.Host
37 + want bool
38 + }{
39 + "matches": {host: testHost("DC1", "Cluster1", "Host1"), want: true},
40 + },
41 + },
42 + "/!*": {
43 + valid: true,
44 + cases: map[string]struct {
45 + host *resources.Host
46 + want bool
47 + }{
48 + "does not match": {host: testHost("DC1", "Cluster1", "Host1"), want: false},
49 + },
50 + },
51 + "/!*/": {
52 + valid: true,
53 + cases: map[string]struct {
54 + host *resources.Host
55 + want bool
56 + }{
57 + "does not match": {host: testHost("DC1", "Cluster1", "Host1"), want: false},
58 + },
59 + },
60 "/!*/ ": {
61 valid: true,
123 - expected: andHostMatcher{
124 - lhs: falseHostDC,
125 - rhs: hostClusterMatcher{matcher.FALSE()},
62 + cases: map[string]struct {
63 + host *resources.Host
64 + want bool
65 + }{
66 + "does not match": {host: testHost("DC1", "Cluster1", "Host1"), want: false},
67 },
68 },
69 "/DC1* DC2* !*/Cluster*": {
70 valid: true,
130 - expected: andHostMatcher{
131 - lhs: hostDCMatcher{mustSP("DC1* DC2* !*")},
132 - rhs: hostClusterMatcher{mustSP("Cluster*")},
71 + cases: map[string]struct {
72 + host *resources.Host
73 + want bool
74 + }{
75 + "matches first datacenter": {host: testHost("DC1A", "Cluster1", "Host1"), want: true},
76 + "matches second datacenter": {host: testHost("DC2A", "Cluster1", "Host1"), want: true},
77 + "rejects other datacenter": {host: testHost("DC3A", "Cluster1", "Host1"), want: false},
78 + "rejects different cluster": {host: testHost("DC1A", "Other", "Host1"), want: false},
79 + "matches any host below path": {host: testHost("DC1A", "Cluster1", "Other"), want: true},
80 + },
81 + },
82 + "/DC1*/Cluster*": {
83 + valid: true,
84 + cases: map[string]struct {
85 + host *resources.Host
86 + want bool
87 + }{
88 + "matches datacenter and cluster": {host: testHost("DC1A", "Cluster1", "Host1"), want: true},
89 + "rejects different datacenter": {host: testHost("DC2A", "Cluster1", "Host1"), want: false},
90 + "rejects different cluster": {host: testHost("DC1A", "Other", "Host1"), want: false},
91 + "matches any host below two levels": {host: testHost("DC1A", "Cluster1", "Other"), want: true},
92 },
93 },
94 "/*/*/HOST1*": {
95 valid: true,
137 - expected: andHostMatcher{
138 - lhs: andHostMatcher{
139 - lhs: trueHostDC,
140 - rhs: hostClusterMatcher{matcher.TRUE()},
141 - },
142 - rhs: hostHostMatcher{mustSP("HOST1*")},
96 + cases: map[string]struct {
97 + host *resources.Host
98 + want bool
99 + }{
100 + "matches host": {host: testHost("DC1", "Cluster1", "HOST10"), want: true},
101 + "rejects different host": {host: testHost("DC1", "Cluster1", "OTHER"), want: false},
102 },
103 },
104 "/*/*/HOST1*/*/*": {
105 valid: true,
147 - expected: andHostMatcher{
148 - lhs: andHostMatcher{
149 - lhs: trueHostDC,
150 - rhs: hostClusterMatcher{matcher.TRUE()},
151 - },
152 - rhs: hostHostMatcher{mustSP("HOST1*")},
106 + cases: map[string]struct {
107 + host *resources.Host
108 + want bool
109 + }{
110 + "ignores extra segments": {host: testHost("DC1", "Cluster1", "HOST10"), want: true},
111 },
112 },
113 "[/DC1*,/DC2*]": {
114 valid: true,
157 - expected: orHostMatcher{
158 - lhs: hostDCMatcher{mustSP("DC1*")},
159 - rhs: hostDCMatcher{mustSP("DC2*")},
115 + cases: map[string]struct {
116 + host *resources.Host
117 + want bool
118 + }{
119 + "matches first include": {host: testHost("DC1A", "Cluster1", "Host1"), want: true},
120 + "matches second include": {host: testHost("DC2A", "Cluster1", "Host1"), want: true},
121 + "rejects other": {host: testHost("DC3A", "Cluster1", "Host1"), want: false},
122 },
123 },
124 "[/DC1*,/DC2*,/DC3*/Cluster1*/H*]": {
125 valid: true,
164 - expected: orHostMatcher{
165 - lhs: orHostMatcher{
166 - lhs: hostDCMatcher{mustSP("DC1*")},
167 - rhs: hostDCMatcher{mustSP("DC2*")},
168 - },
169 - rhs: andHostMatcher{
170 - lhs: andHostMatcher{
171 - lhs: hostDCMatcher{mustSP("DC3*")},
172 - rhs: hostClusterMatcher{mustSP("Cluster1*")},
173 - },
174 - rhs: hostHostMatcher{mustSP("H*")},
175 - },
126 + cases: map[string]struct {
127 + host *resources.Host
128 + want bool
129 + }{
130 + "matches first include": {host: testHost("DC1A", "Other", "Other"), want: true},
131 + "matches second include": {host: testHost("DC2A", "Other", "Other"), want: true},
132 + "matches third include": {host: testHost("DC3A", "Cluster10", "Host1"), want: true},
133 + "rejects third bad host": {host: testHost("DC3A", "Cluster10", "Other"), want: false},
134 + "rejects third bad cluster": {host: testHost("DC3A", "Other", "Host1"), want: false},
135 },
136 },
137 }
@@ -185,7 +144,12 @@ func TestHostIncludes_Parse(t *testing.T) {
144 if !test.valid {
145 assert.Error(t, err)
146 } else {
188 - assert.Equal(t, test.expected, m)
147 + assert.NoError(t, err)
148 + for caseName, tc := range test.cases {
149 + t.Run(caseName, func(t *testing.T) {
150 + assert.Equal(t, tc.want, m.Match(tc.host))
151 + })
152 + }
153 }
154 })
155 }
@@ -193,76 +157,118 @@ func TestHostIncludes_Parse(t *testing.T) {
157
158 func TestVMIncludes_Parse(t *testing.T) {
159 tests := map[string]struct {
196 - valid bool
197 - includes []string
198 - expected VMMatcher
160 + valid bool
161 + cases map[string]struct {
162 + vm *resources.VM
163 + want bool
164 + }
165 }{
166 "": {valid: false},
167 "*/C1/H1/V1": {valid: false},
202 - "/*": {valid: true, expected: trueVMDC},
203 - "/!*": {valid: true, expected: falseVMDC},
204 - "/!*/": {valid: true, expected: falseVMDC},
168 + "/*": {
169 + valid: true,
170 + cases: map[string]struct {
171 + vm *resources.VM
172 + want bool
173 + }{
174 + "matches": {vm: testVM("DC1", "Cluster1", "Host1", "VM1"), want: true},
175 + },
176 + },
177 + "/!*": {
178 + valid: true,
179 + cases: map[string]struct {
180 + vm *resources.VM
181 + want bool
182 + }{
183 + "does not match": {vm: testVM("DC1", "Cluster1", "Host1", "VM1"), want: false},
184 + },
185 + },
186 + "/!*/": {
187 + valid: true,
188 + cases: map[string]struct {
189 + vm *resources.VM
190 + want bool
191 + }{
192 + "does not match": {vm: testVM("DC1", "Cluster1", "Host1", "VM1"), want: false},
193 + },
194 + },
195 "/!*/ ": {
196 valid: true,
207 - expected: andVMMatcher{
208 - lhs: falseVMDC,
209 - rhs: vmClusterMatcher{matcher.FALSE()},
197 + cases: map[string]struct {
198 + vm *resources.VM
199 + want bool
200 + }{
201 + "does not match": {vm: testVM("DC1", "Cluster1", "Host1", "VM1"), want: false},
202 },
203 },
204 "/DC1* DC2* !*/Cluster*": {
205 valid: true,
214 - expected: andVMMatcher{
215 - lhs: vmDCMatcher{mustSP("DC1* DC2* !*")},
216 - rhs: vmClusterMatcher{mustSP("Cluster*")},
206 + cases: map[string]struct {
207 + vm *resources.VM
208 + want bool
209 + }{
210 + "matches first datacenter": {vm: testVM("DC1A", "Cluster1", "Host1", "VM1"), want: true},
211 + "matches second datacenter": {vm: testVM("DC2A", "Cluster1", "Host1", "VM1"), want: true},
212 + "rejects other datacenter": {vm: testVM("DC3A", "Cluster1", "Host1", "VM1"), want: false},
213 + "rejects different cluster": {vm: testVM("DC1A", "Other", "Host1", "VM1"), want: false},
214 + "matches any VM below path": {vm: testVM("DC1A", "Cluster1", "Other", "Other"), want: true},
215 + },
216 + },
217 + "/DC1*/Cluster*": {
218 + valid: true,
219 + cases: map[string]struct {
220 + vm *resources.VM
221 + want bool
222 + }{
223 + "matches datacenter and cluster": {vm: testVM("DC1A", "Cluster1", "Host1", "VM1"), want: true},
224 + "rejects different datacenter": {vm: testVM("DC2A", "Cluster1", "Host1", "VM1"), want: false},
225 + "rejects different cluster": {vm: testVM("DC1A", "Other", "Host1", "VM1"), want: false},
226 + "matches any VM below two levels": {vm: testVM("DC1A", "Cluster1", "Other", "Other"), want: true},
227 },
228 },
229 "/*/*/HOST1": {
230 valid: true,
221 - expected: andVMMatcher{
222 - lhs: andVMMatcher{
223 - lhs: trueVMDC,
224 - rhs: vmClusterMatcher{matcher.TRUE()},
225 - },
226 - rhs: vmHostMatcher{mustSP("HOST1")},
231 + cases: map[string]struct {
232 + vm *resources.VM
233 + want bool
234 + }{
235 + "matches host": {vm: testVM("DC1", "Cluster1", "HOST1", "VM1"), want: true},
236 + "rejects different host": {vm: testVM("DC1", "Cluster1", "HOST2", "VM1"), want: false},
237 + "matches any VM on host": {vm: testVM("DC1", "Cluster1", "HOST1", "Other"), want: true},
238 },
239 },
240 "/*/*/HOST1*/*/*": {
241 valid: true,
231 - expected: andVMMatcher{
232 - lhs: andVMMatcher{
233 - lhs: andVMMatcher{
234 - lhs: trueVMDC,
235 - rhs: vmClusterMatcher{matcher.TRUE()},
236 - },
237 - rhs: vmHostMatcher{mustSP("HOST1*")},
238 - },
239 - rhs: vmVMMatcher{matcher.TRUE()},
242 + cases: map[string]struct {
243 + vm *resources.VM
244 + want bool
245 + }{
246 + "matches host and wildcard VM": {vm: testVM("DC1", "Cluster1", "HOST10", "VM1"), want: true},
247 + "rejects different host": {vm: testVM("DC1", "Cluster1", "Other", "VM1"), want: false},
248 },
249 },
250 "[/DC1*,/DC2*]": {
251 valid: true,
244 - expected: orVMMatcher{
245 - lhs: vmDCMatcher{mustSP("DC1*")},
246 - rhs: vmDCMatcher{mustSP("DC2*")},
252 + cases: map[string]struct {
253 + vm *resources.VM
254 + want bool
255 + }{
256 + "matches first include": {vm: testVM("DC1A", "Cluster1", "Host1", "VM1"), want: true},
257 + "matches second include": {vm: testVM("DC2A", "Cluster1", "Host1", "VM1"), want: true},
258 + "rejects other": {vm: testVM("DC3A", "Cluster1", "Host1", "VM1"), want: false},
259 },
260 },
261 "[/DC1*,/DC2*,/DC3*/Cluster1*/H*/VM*]": {
262 valid: true,
251 - expected: orVMMatcher{
252 - lhs: orVMMatcher{
253 - lhs: vmDCMatcher{mustSP("DC1*")},
254 - rhs: vmDCMatcher{mustSP("DC2*")},
255 - },
256 - rhs: andVMMatcher{
257 - lhs: andVMMatcher{
258 - lhs: andVMMatcher{
259 - lhs: vmDCMatcher{mustSP("DC3*")},
260 - rhs: vmClusterMatcher{mustSP("Cluster1*")},
261 - },
262 - rhs: vmHostMatcher{mustSP("H*")},
263 - },
264 - rhs: vmVMMatcher{mustSP("VM*")},
265 - },
263 + cases: map[string]struct {
264 + vm *resources.VM
265 + want bool
266 + }{
267 + "matches first include": {vm: testVM("DC1A", "Other", "Other", "Other"), want: true},
268 + "matches second include": {vm: testVM("DC2A", "Other", "Other", "Other"), want: true},
269 + "matches third include": {vm: testVM("DC3A", "Cluster10", "Host1", "VM1"), want: true},
270 + "rejects third bad VM": {vm: testVM("DC3A", "Cluster10", "Host1", "Other"), want: false},
271 + "rejects third bad host": {vm: testVM("DC3A", "Cluster10", "Other", "VM1"), want: false},
272 },
273 },
274 }
@@ -275,104 +281,114 @@ func TestVMIncludes_Parse(t *testing.T) {
281 if !test.valid {
282 assert.Error(t, err)
283 } else {
278 - assert.Equal(t, test.expected, m)
284 + assert.NoError(t, err)
285 + for caseName, tc := range test.cases {
286 + t.Run(caseName, func(t *testing.T) {
287 + assert.Equal(t, tc.want, m.Match(tc.vm))
288 + })
289 + }
290 }
291 })
292 }
293 }
294
284 -func TestOrDSMatcher_Match(t *testing.T) {
285 - tests := map[string]struct {
286 - expected bool
287 - lhs DatastoreMatcher
288 - rhs DatastoreMatcher
289 - }{
290 - "true, true": {expected: true, lhs: trueDSDC, rhs: trueDSDC},
291 - "true, false": {expected: true, lhs: trueDSDC, rhs: falseDSDC},
292 - "false, true": {expected: true, lhs: falseDSDC, rhs: trueDSDC},
293 - "false, false": {expected: false, lhs: falseDSDC, rhs: falseDSDC},
294 - }
295 -
296 - var ds resources.Datastore
297 - for name, test := range tests {
298 - t.Run(name, func(t *testing.T) {
299 - m := orDSMatcher{lhs: test.lhs, rhs: test.rhs}
300 - assert.Equal(t, test.expected, m.Match(&ds))
301 - })
302 - }
303 -}
304 -
305 -func TestAndDSMatcher_Match(t *testing.T) {
306 - tests := map[string]struct {
307 - expected bool
308 - lhs DatastoreMatcher
309 - rhs DatastoreMatcher
310 - }{
311 - "true, true": {expected: true, lhs: trueDSDC, rhs: trueDSDC},
312 - "true, false": {expected: false, lhs: trueDSDC, rhs: falseDSDC},
313 - "false, true": {expected: false, lhs: falseDSDC, rhs: trueDSDC},
314 - "false, false": {expected: false, lhs: falseDSDC, rhs: falseDSDC},
315 - }
316 -
317 - var ds resources.Datastore
318 - for name, test := range tests {
319 - t.Run(name, func(t *testing.T) {
320 - m := andDSMatcher{lhs: test.lhs, rhs: test.rhs}
321 - assert.Equal(t, test.expected, m.Match(&ds))
322 - })
323 - }
324 -}
325 -
295 func TestDatastoreIncludes_Parse(t *testing.T) {
296 tests := map[string]struct {
328 - valid bool
329 - expected DatastoreMatcher
297 + valid bool
298 + cases map[string]struct {
299 + ds *resources.Datastore
300 + want bool
301 + }
302 }{
303 "": {valid: false},
304 "*/DS1": {valid: false},
333 - "/": {valid: true, expected: falseDSDC},
334 - "/*": {valid: true, expected: trueDSDC},
335 - "/!*": {valid: true, expected: falseDSDC},
336 - "/!*/": {valid: true, expected: falseDSDC},
305 + "/": {
306 + valid: true,
307 + cases: map[string]struct {
308 + ds *resources.Datastore
309 + want bool
310 + }{
311 + "does not match": {ds: testDatastore("DC1", "DS1"), want: false},
312 + },
313 + },
314 + "/*": {
315 + valid: true,
316 + cases: map[string]struct {
317 + ds *resources.Datastore
318 + want bool
319 + }{
320 + "matches": {ds: testDatastore("DC1", "DS1"), want: true},
321 + },
322 + },
323 + "/!*": {
324 + valid: true,
325 + cases: map[string]struct {
326 + ds *resources.Datastore
327 + want bool
328 + }{
329 + "does not match": {ds: testDatastore("DC1", "DS1"), want: false},
330 + },
331 + },
332 + "/!*/": {
333 + valid: true,
334 + cases: map[string]struct {
335 + ds *resources.Datastore
336 + want bool
337 + }{
338 + "does not match": {ds: testDatastore("DC1", "DS1"), want: false},
339 + },
340 + },
341 "/!*/ ": {
342 valid: true,
339 - expected: andDSMatcher{
340 - lhs: falseDSDC,
341 - rhs: dsDSMatcher{matcher.FALSE()},
343 + cases: map[string]struct {
344 + ds *resources.Datastore
345 + want bool
346 + }{
347 + "does not match": {ds: testDatastore("DC1", "DS1"), want: false},
348 },
349 },
350 "/DC1*/DS*": {
351 valid: true,
346 - expected: andDSMatcher{
347 - lhs: dsDCMatcher{mustSP("DC1*")},
348 - rhs: dsDSMatcher{mustSP("DS*")},
352 + cases: map[string]struct {
353 + ds *resources.Datastore
354 + want bool
355 + }{
356 + "matches datacenter and datastore": {ds: testDatastore("DC1A", "DS1"), want: true},
357 + "rejects different datacenter": {ds: testDatastore("DC2A", "DS1"), want: false},
358 + "rejects different datastore": {ds: testDatastore("DC1A", "Other"), want: false},
359 },
360 },
361 "/*/*/extra": {
362 valid: true,
353 - expected: andDSMatcher{
354 - lhs: trueDSDC,
355 - rhs: dsDSMatcher{matcher.TRUE()},
363 + cases: map[string]struct {
364 + ds *resources.Datastore
365 + want bool
366 + }{
367 + "ignores extra segments": {ds: testDatastore("DC1", "DS1"), want: true},
368 },
369 },
370 "[/DC1*,/DC2*]": {
371 valid: true,
360 - expected: orDSMatcher{
361 - lhs: dsDCMatcher{mustSP("DC1*")},
362 - rhs: dsDCMatcher{mustSP("DC2*")},
372 + cases: map[string]struct {
373 + ds *resources.Datastore
374 + want bool
375 + }{
376 + "matches first include": {ds: testDatastore("DC1A", "DS1"), want: true},
377 + "matches second include": {ds: testDatastore("DC2A", "DS1"), want: true},
378 + "rejects other": {ds: testDatastore("DC3A", "DS1"), want: false},
379 },
380 },
381 "[/DC1*,/DC2*,/DC3*/DS*]": {
382 valid: true,
367 - expected: orDSMatcher{
368 - lhs: orDSMatcher{
369 - lhs: dsDCMatcher{mustSP("DC1*")},
370 - rhs: dsDCMatcher{mustSP("DC2*")},
371 - },
372 - rhs: andDSMatcher{
373 - lhs: dsDCMatcher{mustSP("DC3*")},
374 - rhs: dsDSMatcher{mustSP("DS*")},
375 - },
383 + cases: map[string]struct {
384 + ds *resources.Datastore
385 + want bool
386 + }{
387 + "matches first include": {ds: testDatastore("DC1A", "Other"), want: true},
388 + "matches second include": {ds: testDatastore("DC2A", "Other"), want: true},
389 + "matches third include": {ds: testDatastore("DC3A", "DS1"), want: true},
390 + "rejects third bad datastore": {ds: testDatastore("DC3A", "Other"), want: false},
391 + "rejects third bad datacenter": {ds: testDatastore("Other", "DS1"), want: false},
392 },
393 },
394 }
@@ -385,104 +401,114 @@ func TestDatastoreIncludes_Parse(t *testing.T) {
401 if !test.valid {
402 assert.Error(t, err)
403 } else {
388 - assert.Equal(t, test.expected, m)
404 + assert.NoError(t, err)
405 + for caseName, tc := range test.cases {
406 + t.Run(caseName, func(t *testing.T) {
407 + assert.Equal(t, tc.want, m.Match(tc.ds))
408 + })
409 + }
410 }
411 })
412 }
413 }
414
394 -func TestOrClusterMatcher_Match(t *testing.T) {
395 - tests := map[string]struct {
396 - expected bool
397 - lhs ClusterMatcher
398 - rhs ClusterMatcher
399 - }{
400 - "true, true": {expected: true, lhs: trueClDC, rhs: trueClDC},
401 - "true, false": {expected: true, lhs: trueClDC, rhs: falseClDC},
402 - "false, true": {expected: true, lhs: falseClDC, rhs: trueClDC},
403 - "false, false": {expected: false, lhs: falseClDC, rhs: falseClDC},
404 - }
405 -
406 - var cl resources.Cluster
407 - for name, test := range tests {
408 - t.Run(name, func(t *testing.T) {
409 - m := orClusterMatcher{lhs: test.lhs, rhs: test.rhs}
410 - assert.Equal(t, test.expected, m.Match(&cl))
411 - })
412 - }
413 -}
414 -
415 -func TestAndClusterMatcher_Match(t *testing.T) {
416 - tests := map[string]struct {
417 - expected bool
418 - lhs ClusterMatcher
419 - rhs ClusterMatcher
420 - }{
421 - "true, true": {expected: true, lhs: trueClDC, rhs: trueClDC},
422 - "true, false": {expected: false, lhs: trueClDC, rhs: falseClDC},
423 - "false, true": {expected: false, lhs: falseClDC, rhs: trueClDC},
424 - "false, false": {expected: false, lhs: falseClDC, rhs: falseClDC},
425 - }
426 -
427 - var cl resources.Cluster
428 - for name, test := range tests {
429 - t.Run(name, func(t *testing.T) {
430 - m := andClusterMatcher{lhs: test.lhs, rhs: test.rhs}
431 - assert.Equal(t, test.expected, m.Match(&cl))
432 - })
433 - }
434 -}
435 -
415 func TestClusterIncludes_Parse(t *testing.T) {
416 tests := map[string]struct {
438 - valid bool
439 - expected ClusterMatcher
417 + valid bool
418 + cases map[string]struct {
419 + cluster *resources.Cluster
420 + want bool
421 + }
422 }{
423 "": {valid: false},
424 "*/C1": {valid: false},
443 - "/": {valid: true, expected: falseClDC},
444 - "/*": {valid: true, expected: trueClDC},
445 - "/!*": {valid: true, expected: falseClDC},
446 - "/!*/": {valid: true, expected: falseClDC},
425 + "/": {
426 + valid: true,
427 + cases: map[string]struct {
428 + cluster *resources.Cluster
429 + want bool
430 + }{
431 + "does not match": {cluster: testCluster("DC1", "Cluster1"), want: false},
432 + },
433 + },
434 + "/*": {
435 + valid: true,
436 + cases: map[string]struct {
437 + cluster *resources.Cluster
438 + want bool
439 + }{
440 + "matches": {cluster: testCluster("DC1", "Cluster1"), want: true},
441 + },
442 + },
443 + "/!*": {
444 + valid: true,
445 + cases: map[string]struct {
446 + cluster *resources.Cluster
447 + want bool
448 + }{
449 + "does not match": {cluster: testCluster("DC1", "Cluster1"), want: false},
450 + },
451 + },
452 + "/!*/": {
453 + valid: true,
454 + cases: map[string]struct {
455 + cluster *resources.Cluster
456 + want bool
457 + }{
458 + "does not match": {cluster: testCluster("DC1", "Cluster1"), want: false},
459 + },
460 + },
461 "/!*/ ": {
462 valid: true,
449 - expected: andClusterMatcher{
450 - lhs: falseClDC,
451 - rhs: clusterNameMatcher{matcher.FALSE()},
463 + cases: map[string]struct {
464 + cluster *resources.Cluster
465 + want bool
466 + }{
467 + "does not match": {cluster: testCluster("DC1", "Cluster1"), want: false},
468 },
469 },
470 "/DC1*/Cluster*": {
471 valid: true,
456 - expected: andClusterMatcher{
457 - lhs: clusterDCMatcher{mustSP("DC1*")},
458 - rhs: clusterNameMatcher{mustSP("Cluster*")},
472 + cases: map[string]struct {
473 + cluster *resources.Cluster
474 + want bool
475 + }{
476 + "matches datacenter and cluster": {cluster: testCluster("DC1A", "Cluster1"), want: true},
477 + "rejects different datacenter": {cluster: testCluster("DC2A", "Cluster1"), want: false},
478 + "rejects different cluster": {cluster: testCluster("DC1A", "Other"), want: false},
479 },
480 },
481 "/*/*/extra": {
482 valid: true,
463 - expected: andClusterMatcher{
464 - lhs: trueClDC,
465 - rhs: clusterNameMatcher{matcher.TRUE()},
483 + cases: map[string]struct {
484 + cluster *resources.Cluster
485 + want bool
486 + }{
487 + "ignores extra segments": {cluster: testCluster("DC1", "Cluster1"), want: true},
488 },
489 },
490 "[/DC1*,/DC2*]": {
491 valid: true,
470 - expected: orClusterMatcher{
471 - lhs: clusterDCMatcher{mustSP("DC1*")},
472 - rhs: clusterDCMatcher{mustSP("DC2*")},
492 + cases: map[string]struct {
493 + cluster *resources.Cluster
494 + want bool
495 + }{
496 + "matches first include": {cluster: testCluster("DC1A", "Cluster1"), want: true},
497 + "matches second include": {cluster: testCluster("DC2A", "Cluster1"), want: true},
498 + "rejects other": {cluster: testCluster("DC3A", "Cluster1"), want: false},
499 },
500 },
501 "[/DC1*,/DC2*,/DC3*/Cluster*]": {
502 valid: true,
477 - expected: orClusterMatcher{
478 - lhs: orClusterMatcher{
479 - lhs: clusterDCMatcher{mustSP("DC1*")},
480 - rhs: clusterDCMatcher{mustSP("DC2*")},
481 - },
482 - rhs: andClusterMatcher{
483 - lhs: clusterDCMatcher{mustSP("DC3*")},
484 - rhs: clusterNameMatcher{mustSP("Cluster*")},
485 - },
503 + cases: map[string]struct {
504 + cluster *resources.Cluster
505 + want bool
506 + }{
507 + "matches first include": {cluster: testCluster("DC1A", "Other"), want: true},
508 + "matches second include": {cluster: testCluster("DC2A", "Other"), want: true},
509 + "matches third include": {cluster: testCluster("DC3A", "Cluster1"), want: true},
510 + "rejects third bad cluster": {cluster: testCluster("DC3A", "Other"), want: false},
511 + "rejects third bad datacenter": {cluster: testCluster("Other", "Cluster1"), want: false},
512 },
513 },
514 }
@@ -495,17 +521,207 @@ func TestClusterIncludes_Parse(t *testing.T) {
521 if !test.valid {
522 assert.Error(t, err)
523 } else {
498 - assert.Equal(t, test.expected, m)
524 + assert.NoError(t, err)
525 + for caseName, tc := range test.cases {
526 + t.Run(caseName, func(t *testing.T) {
527 + assert.Equal(t, tc.want, m.Match(tc.cluster))
528 + })
529 + }
530 + }
531 + })
532 + }
533 +}
534 +
535 +func TestInventoryIncludes_ParseEmptyReturnsNil(t *testing.T) {
536 + tests := map[string]struct {
537 + parse func() (any, error)
538 + }{
539 + "host": {parse: func() (any, error) { return HostIncludes{}.Parse() }},
540 + "VM": {parse: func() (any, error) { return VMIncludes{}.Parse() }},
541 + "datastore": {parse: func() (any, error) { return DatastoreIncludes{}.Parse() }},
542 + "cluster": {parse: func() (any, error) { return ClusterIncludes{}.Parse() }},
543 + }
544 +
545 + for name, tc := range tests {
546 + t.Run(name, func(t *testing.T) {
547 + m, err := tc.parse()
548 +
549 + assert.NoError(t, err)
550 + assert.Nil(t, m)
551 + })
552 + }
553 +}
554 +
555 +func TestDatastoreClusterIncludes_Parse(t *testing.T) {
556 + pod := &resources.StoragePod{
557 + ID: "group-p1",
558 + Name: "Pod1",
559 + Hier: resources.StoragePodHierarchy{DC: resources.HierarchyValue{Name: "DC1"}},
560 + }
561 + tests := map[string]struct {
562 + includes DatastoreClusterIncludes
563 + want bool
564 + wantErr bool
565 + }{
566 + "invalid pattern": {includes: DatastoreClusterIncludes{"["}, wantErr: true},
567 + "path match": {includes: DatastoreClusterIncludes{"/DC1/Pod1"}, want: true},
568 + "name match": {includes: DatastoreClusterIncludes{"Pod1"}, want: true},
569 + "id match": {includes: DatastoreClusterIncludes{"group-p1"}, want: true},
570 + "no match": {includes: DatastoreClusterIncludes{"Pod2"}, want: false},
571 + }
572 +
573 + for name, tc := range tests {
574 + t.Run(name, func(t *testing.T) {
575 + m, err := tc.includes.Parse()
576 +
577 + if tc.wantErr {
578 + assert.Error(t, err)
579 + return
580 + }
581 + assert.NoError(t, err)
582 + assert.Equal(t, tc.want, m.Match(pod))
583 + })
584 + }
585 +}
586 +
587 +func TestVSANClusterIncludes_Parse(t *testing.T) {
588 + cluster := &resources.Cluster{
589 + ID: "domain-c1",
590 + Name: "Cluster1",
591 + Hier: resources.ClusterHierarchy{DC: resources.HierarchyValue{Name: "DC1"}},
592 + VSANUUID: "cluster-uuid",
593 + }
594 + tests := map[string]struct {
595 + includes VSANClusterIncludes
596 + want bool
597 + wantErr bool
598 + }{
599 + "invalid pattern": {includes: VSANClusterIncludes{"["}, wantErr: true},
600 + "path match": {includes: VSANClusterIncludes{"/DC1/Cluster1"}, want: true},
601 + "name match": {includes: VSANClusterIncludes{"Cluster1"}, want: true},
602 + "id match": {includes: VSANClusterIncludes{"domain-c1"}, want: true},
603 + "uuid match": {includes: VSANClusterIncludes{"vsan_uuid:cluster-uuid"}, want: true},
604 + "no match": {includes: VSANClusterIncludes{"Cluster2"}, want: false},
605 + }
606 +
607 + for name, tc := range tests {
608 + t.Run(name, func(t *testing.T) {
609 + m, err := tc.includes.Parse()
610 +
611 + if tc.wantErr {
612 + assert.Error(t, err)
613 + return
614 }
615 + assert.NoError(t, err)
616 + assert.Equal(t, tc.want, m.Match(cluster))
617 })
618 }
619 }
620
621 +func TestVSANHostIncludes_Parse(t *testing.T) {
622 + host := &resources.Host{
623 + ID: "host-1",
624 + Name: "Host1",
625 + Hier: resources.HostHierarchy{DC: resources.HierarchyValue{Name: "DC1"}, Cluster: resources.HierarchyValue{Name: "Cluster1"}},
626 + VSANNodeUUID: "host-uuid",
627 + }
628 + tests := map[string]struct {
629 + includes VSANHostIncludes
630 + want bool
631 + wantErr bool
632 + }{
633 + "invalid pattern": {includes: VSANHostIncludes{"["}, wantErr: true},
634 + "path match": {includes: VSANHostIncludes{"/DC1/Cluster1/Host1"}, want: true},
635 + "name match": {includes: VSANHostIncludes{"Host1"}, want: true},
636 + "id match": {includes: VSANHostIncludes{"host-1"}, want: true},
637 + "uuid match": {includes: VSANHostIncludes{"vsan_node_uuid:host-uuid"}, want: true},
638 + "no match": {includes: VSANHostIncludes{"Host2"}, want: false},
639 + }
640 +
641 + for name, tc := range tests {
642 + t.Run(name, func(t *testing.T) {
643 + m, err := tc.includes.Parse()
644 +
645 + if tc.wantErr {
646 + assert.Error(t, err)
647 + return
648 + }
649 + assert.NoError(t, err)
650 + assert.Equal(t, tc.want, m.Match(host))
651 + })
652 + }
653 +}
654 +
655 +func TestVSANVMIncludes_Parse(t *testing.T) {
656 + vm := &resources.VM{
657 + ID: "vm-1",
658 + Name: "VM1",
659 + Hier: resources.VMHierarchy{DC: resources.HierarchyValue{Name: "DC1"}, Cluster: resources.HierarchyValue{Name: "Cluster1"}, Host: resources.HierarchyValue{Name: "Host1"}},
660 + InstanceUUID: "vm-uuid",
661 + }
662 + tests := map[string]struct {
663 + includes VSANVMIncludes
664 + want bool
665 + wantErr bool
666 + }{
667 + "invalid pattern": {includes: VSANVMIncludes{"["}, wantErr: true},
668 + "path match": {includes: VSANVMIncludes{"/DC1/Cluster1/Host1/VM1"}, want: true},
669 + "name match": {includes: VSANVMIncludes{"VM1"}, want: true},
670 + "id match": {includes: VSANVMIncludes{"vm-1"}, want: true},
671 + "uuid match": {includes: VSANVMIncludes{"instance_uuid:vm-uuid"}, want: true},
672 + "no match": {includes: VSANVMIncludes{"VM2"}, want: false},
673 + }
674 +
675 + for name, tc := range tests {
676 + t.Run(name, func(t *testing.T) {
677 + m, err := tc.includes.Parse()
678 +
679 + if tc.wantErr {
680 + assert.Error(t, err)
681 + return
682 + }
683 + assert.NoError(t, err)
684 + assert.Equal(t, tc.want, m.Match(vm))
685 + })
686 + }
687 +}
688 +
689 +func testHost(dc, cluster, name string) *resources.Host {
690 + return &resources.Host{
691 + Name: name,
692 + Hier: resources.HostHierarchy{
693 + DC: resources.HierarchyValue{Name: dc},
694 + Cluster: resources.HierarchyValue{Name: cluster},
695 + },
696 + }
697 +}
698 +
699 +func testVM(dc, cluster, host, name string) *resources.VM {
700 + return &resources.VM{
701 + Name: name,
702 + Hier: resources.VMHierarchy{
703 + DC: resources.HierarchyValue{Name: dc},
704 + Cluster: resources.HierarchyValue{Name: cluster},
705 + Host: resources.HierarchyValue{Name: host},
706 + },
707 + }
708 +}
709 +
710 +func testDatastore(dc, name string) *resources.Datastore {
711 + return &resources.Datastore{
712 + Name: name,
713 + Hier: resources.DatastoreHierarchy{DC: resources.HierarchyValue{Name: dc}},
714 + }
715 +}
716 +
717 +func testCluster(dc, name string) *resources.Cluster {
718 + return &resources.Cluster{
719 + Name: name,
720 + Hier: resources.ClusterHierarchy{DC: resources.HierarchyValue{Name: dc}},
721 + }
722 +}
723 +
724 func prepareIncludes(include string) []string {
725 trimmed := strings.Trim(include, "[]")
726 return strings.Split(trimmed, ",")
727 }
508 -
509 -func mustSP(expr string) matcher.Matcher {
510 - return matcher.Must(matcher.NewSimplePatternsMatcher(expr))
511 -}
src/go/plugin/go.d/collector/vsphere/metadata.yaml
+821 -96
@@ -22,8 +22,18 @@ modules:
22 overview:
23 data_collection:
24 metrics_description: |
25 - This collector monitors hosts, VMs, datastores, clusters, and resource pools from `vCenter` servers.
26 -
25 + Monitors vSphere resources from `vCenter` servers.
26 +
27 + Includes hosts, VMs, datastores, clusters, resource pools,
28 + and inventory counts.
29 +
30 + Use the `vcsa` collector for vCenter Server Appliance health.
31 +
32 + Use the `snmp` collector with the `vmware-esx` profile for
33 + ESXi hardware, HBA, and environment sensors.
34 +
35 + Those surfaces are intentionally not duplicated here by default.
36 +
37 > **Warning**: The `vsphere` collector cannot re-login and continue collecting metrics after a vCenter reboot.
38 > go.d.plugin needs to be restarted.
39 method_description: ""
@@ -47,45 +57,28 @@ modules:
57
58 It is likely that 20 seconds is not enough for big installations and the value should be tuned.
59
50 - To get a better view we recommend running the collector in debug mode and seeing how much time it will take to collect metrics.
51 -
52 - <details>
53 - <summary>Example (all not related debug lines were removed)</summary>
54 -
55 - ```
56 - [ilyam@pc]$ ./go.d.plugin -d -m vsphere
57 - [ DEBUG ] vsphere[vsphere] discover.go:94 discovering : starting resource discovering process
58 - [ DEBUG ] vsphere[vsphere] discover.go:102 discovering : found 3 dcs, process took 49.329656ms
59 - [ DEBUG ] vsphere[vsphere] discover.go:109 discovering : found 12 folders, process took 49.538688ms
60 - [ DEBUG ] vsphere[vsphere] discover.go:116 discovering : found 3 clusters, process took 47.722692ms
61 - [ DEBUG ] vsphere[vsphere] discover.go:123 discovering : found 2 hosts, process took 52.966995ms
62 - [ DEBUG ] vsphere[vsphere] discover.go:130 discovering : found 2 vms, process took 49.832979ms
63 - [ INFO ] vsphere[vsphere] discover.go:140 discovering : found 3 dcs, 12 folders, 3 clusters (2 dummy), 2 hosts, 3 vms, process took 249.655993ms
64 - [ DEBUG ] vsphere[vsphere] build.go:12 discovering : building : starting building resources process
65 - [ INFO ] vsphere[vsphere] build.go:23 discovering : building : built 3/3 dcs, 12/12 folders, 3/3 clusters, 2/2 hosts, 3/3 vms, process took 63.3µs
66 - [ DEBUG ] vsphere[vsphere] hierarchy.go:10 discovering : hierarchy : start setting resources hierarchy process
67 - [ INFO ] vsphere[vsphere] hierarchy.go:18 discovering : hierarchy : set 3/3 clusters, 2/2 hosts, 3/3 vms, process took 6.522µs
68 - [ DEBUG ] vsphere[vsphere] filter.go:24 discovering : filtering : starting filtering resources process
69 - [ DEBUG ] vsphere[vsphere] filter.go:45 discovering : filtering : removed 0 unmatched hosts
70 - [ DEBUG ] vsphere[vsphere] filter.go:56 discovering : filtering : removed 0 unmatched vms
71 - [ INFO ] vsphere[vsphere] filter.go:29 discovering : filtering : filtered 0/2 hosts, 0/3 vms, process took 42.973µs
72 - [ DEBUG ] vsphere[vsphere] metric_lists.go:14 discovering : metric lists : starting resources metric lists collection process
73 - [ INFO ] vsphere[vsphere] metric_lists.go:30 discovering : metric lists : collected metric lists for 2/2 hosts, 3/3 vms, process took 275.60764ms
74 - [ INFO ] vsphere[vsphere] discover.go:74 discovering : discovered 2/2 hosts, 3/3 vms, the whole process took 525.614041ms
75 - [ INFO ] vsphere[vsphere] discover.go:11 starting discovery process, will do discovery every 5m0s
76 - [ DEBUG ] vsphere[vsphere] collect.go:11 starting collection process
77 - [ DEBUG ] vsphere[vsphere] scrape.go:48 scraping : scraped metrics for 2/2 hosts, process took 96.257374ms
78 - [ DEBUG ] vsphere[vsphere] scrape.go:60 scraping : scraped metrics for 3/3 vms, process took 57.879697ms
79 - [ DEBUG ] vsphere[vsphere] collect.go:23 metrics collected, process took 154.77997ms
80 - ```
81 -
82 - </details>
83 -
84 - There you can see that discovering took `525.614041ms`, and collecting metrics took `154.77997ms`. Discovering is a separate thread, it doesn't affect collecting.
85 - `update_every` and `timeout` parameters should be adjusted based on these numbers.
60 + To size a job, run the collector in debug mode and compare the discovery and collection timing lines. Discovery runs in a separate goroutine, while collection timing must stay comfortably below `update_every`.
61 +
62 + Useful log lines include:
63 +
64 + - `discovering : discovered ... the whole process took ...`
65 + - `scraping : scraped metrics for ... hosts, process took ...`
66 + - `scraping : scraped metrics for ... vms, process took ...`
67 + - `metrics collected, process took ...`
68 +
69 + Adjust `update_every` and `timeout` based on those timings and on the number of enabled optional surfaces.
70 setup:
71 prerequisites:
88 - list: []
72 + list:
73 + - title: vCenter read-only access
74 + description: |
75 + Configure a vCenter account that can read inventory objects, properties, and performance counters for the datacenters, clusters, ESXi hosts, VMs, datastores, and resource pools selected by the include filters.
76 + - title: Optional vSphere metadata permissions
77 + description: |
78 + `tag_categories` requires access to the vSphere Automation/CIS tagging APIs for the selected categories. `custom_attributes` requires access to custom field definitions and values for the selected inventory objects.
79 + - title: Optional datastore cluster, vSAN, and network data
80 + description: |
81 + `collect_datastore_clusters` requires read access to StoragePod objects. `collect_vsan` requires vSAN Management API access and the vSAN Performance Service on the target clusters. `collect_network_topology` requires read access to Network and Distributed Virtual Port Group inventory objects.
82 configuration:
83 file:
84 name: go.d/vsphere.conf
@@ -103,8 +96,8 @@ modules:
96 group: Collection
97
98 - name: autodetection_retry
106 - description: Autodetection retry interval (seconds). Set 0 to disable.
107 - default_value: 0
99 + description: Autodetection retry interval (seconds).
100 + default_value: 60
101 required: false
102 group: Collection
103
@@ -124,6 +117,149 @@ modules:
117 default_value: 300
118 required: false
119 group: Discovery
120 + - name: tag_categories
121 + description: vSphere tag category allowlist.
122 + default_value: ""
123 + required: false
124 + group: Labels
125 + detailed_description: |
126 + Disabled by default because vSphere tags are user-defined metadata
127 + and can expose internal names, ownership, business unit, or
128 + environment details. Each list item is one glob pattern matching
129 + vSphere tag category names, so names with spaces are supported.
130 + Use `*` only when every tag category is intentional.
131 +
132 + Matching categories are exposed as labels named
133 + `vsphere_tag_<sanitized_category>`. When a resource has multiple
134 + tags in the same category, values are sorted and joined with the
135 + pipe character.
136 +
137 + ```yaml
138 + tag_categories:
139 + - "Environment"
140 + - "Business Unit"
141 + ```
142 + - name: custom_attributes
143 + description: vSphere custom attribute allowlist.
144 + default_value: ""
145 + required: false
146 + group: Labels
147 + detailed_description: |
148 + Disabled by default because vSphere custom attributes are
149 + user-defined metadata and can expose internal names, ownership,
150 + business unit, operational data, or secrets stored by administrators.
151 + Custom attribute values are sent verbatim as labels. Each list
152 + item is one glob pattern matching custom attribute names, so names
153 + with spaces are supported. Use `*` only when every custom attribute
154 + is intentional and none of the matched values contain secrets.
155 +
156 + Matching attributes are exposed as labels named
157 + `vsphere_custom_attribute_<sanitized_name>`.
158 +
159 + ```yaml
160 + custom_attributes:
161 + - "Owner"
162 + - "Cost Center"
163 + ```
164 + - name: collect_datastore_clusters
165 + description: Collect datastore cluster capacity and Storage DRS status.
166 + default_value: no
167 + required: false
168 + group: High Cardinality
169 + detailed_description: |
170 + Disabled by default because it adds a separate vSphere resource
171 + class (`StoragePod`) to the collector output. When enabled, the
172 + collector emits aggregate datastore-cluster capacity, utilization,
173 + and Storage DRS status.
174 + - name: datastore_cluster_include
175 + description: Datastore cluster selector.
176 + default_value: "/*"
177 + required: false
178 + group: High Cardinality
179 + detailed_description: |
180 + Applies only when `collect_datastore_clusters` is enabled. Values
181 + use Netdata simple patterns and match
182 + `/Datacenter/DatastoreCluster`, the datastore-cluster name, or
183 + the vSphere managed object ID. Matching datastore clusters are
184 + included in metrics, labels, cached discovery state, and topology
185 + function output.
186 +
187 + ```yaml
188 + datastore_cluster_include:
189 + - "/*"
190 + ```
191 + - name: collect_vsan
192 + description: Collect vSAN metrics.
193 + default_value: no
194 + required: false
195 + group: High Cardinality
196 + detailed_description: |
197 + Disabled by default because it uses the vSAN Management API and
198 + vSAN Performance Service, and adds extra vCenter queries. When
199 + enabled, it emits vSAN cluster capacity, vSAN cluster health, and
200 + vSAN cluster, host, and VM performance metrics for discovered
201 + vSAN-enabled clusters. Use the vSAN selectors below to choose the
202 + concrete vSAN performance entity refs queried. vSAN events are
203 + not collected by this option.
204 + - name: vsan_cluster_include
205 + description: vSAN cluster selector.
206 + default_value: "/*"
207 + required: false
208 + group: High Cardinality
209 + detailed_description: |
210 + Applies only when `collect_vsan` is enabled. Values use Netdata
211 + simple patterns and match `/Datacenter/Cluster`, the cluster
212 + name, the vSphere managed object ID, or `vsan_uuid:<uuid>`.
213 +
214 + ```yaml
215 + vsan_cluster_include:
216 + - "/*"
217 + - "vsan_uuid:52b..."
218 + ```
219 + - name: vsan_host_include
220 + description: vSAN host selector.
221 + default_value: "/*"
222 + required: false
223 + group: High Cardinality
224 + detailed_description: |
225 + Applies only when `collect_vsan` is enabled. Values use Netdata
226 + simple patterns and match `/Datacenter/Cluster/Host`, the host
227 + name, the vSphere managed object ID, or
228 + `vsan_node_uuid:<uuid>`.
229 +
230 + ```yaml
231 + vsan_host_include:
232 + - "/*"
233 + - "vsan_node_uuid:52b..."
234 + ```
235 + - name: vsan_vm_include
236 + description: vSAN VM selector.
237 + default_value: "/*"
238 + required: false
239 + group: High Cardinality
240 + detailed_description: |
241 + Applies only when `collect_vsan` is enabled. Values use Netdata
242 + simple patterns and match `/Datacenter/Cluster/Host/VM`, the VM
243 + name, the vSphere managed object ID, or
244 + `instance_uuid:<uuid>`.
245 +
246 + ```yaml
247 + vsan_vm_include:
248 + - "/*"
249 + - "instance_uuid:52b..."
250 + ```
251 + - name: collect_network_topology
252 + description: Discover networks for the vSphere Topology function.
253 + default_value: no
254 + required: false
255 + group: Collection
256 + detailed_description: |
257 + Disabled by default to avoid extra vCenter discovery calls for
258 + existing users. When enabled, the collector discovers vSphere
259 + Network and Distributed Virtual Port Group objects and includes
260 + their cached accessibility/status and host/VM relationships in
261 + the vSphere Topology function. It does not create charts or
262 + metrics.
263
264 - name: host_include
265 description: Hosts selector (filter).
@@ -132,7 +268,7 @@ modules:
268 group: Filters
269 detailed_description: |
270 Metrics of hosts matching the selector will be collected.
135 -
271 +
272 - Include pattern syntax: "/Datacenter pattern/Cluster pattern/Host pattern".
273 - Match pattern syntax: [simple patterns](/src/libnetdata/simple_pattern/README.md#simple-patterns).
274 - Syntax:
@@ -206,11 +342,6 @@ modules:
342 default_value: ""
343 required: true
344 group: HTTP Auth
209 - - name: bearer_token_file
210 - description: "Path to a file containing a bearer token (used for `Authorization: Bearer`)."
211 - default_value: ""
212 - required: false
213 - group: HTTP Auth
345
346 - name: tls_skip_verify
347 description: Skip TLS certificate and hostname verification (insecure).
@@ -233,48 +364,6 @@ modules:
364 required: false
365 group: TLS
366
236 - - name: proxy_url
237 - description: HTTP proxy URL.
238 - default_value: ""
239 - required: false
240 - group: Proxy
241 - - name: proxy_username
242 - description: Username for proxy Basic HTTP authentication.
243 - default_value: ""
244 - required: false
245 - group: Proxy
246 - - name: proxy_password
247 - description: Password for proxy Basic HTTP authentication.
248 - default_value: ""
249 - required: false
250 - group: Proxy
251 -
252 - - name: method
253 - description: HTTP method to use.
254 - default_value: "GET"
255 - required: false
256 - group: Request
257 - - name: body
258 - description: Request body (e.g., for POST/PUT).
259 - default_value: ""
260 - required: false
261 - group: Request
262 - - name: headers
263 - description: "Additional HTTP headers (one per line as key: value)."
264 - default_value: ""
265 - required: false
266 - group: Request
267 - - name: not_follow_redirects
268 - description: Do not follow HTTP redirects.
269 - default_value: no
270 - required: false
271 - group: Request
272 - - name: force_http2
273 - description: Force HTTP/2 (including h2c over TCP).
274 - default_value: no
275 - required: false
276 - group: Request
277 -
367 - name: vnode
368 description: Associates this data collection job with a [Virtual Node](https://learn.netdata.cloud/docs/netdata-agent/configuration/organize-systems-metrics-and-alerts#virtual-nodes).
369 default_value: ""
@@ -298,7 +387,7 @@ modules:
387 - name: Multi-instance
388 description: |
389 > **Note**: When you define multiple jobs, their names must be unique.
301 -
390 +
391 Collecting metrics from local and remote instances.
392 config: |
393 jobs:
@@ -313,16 +402,33 @@ modules:
402 password : somepassword
403 troubleshooting:
404 problems:
316 - list: []
405 + list:
406 + - name: Missing performance samples
407 + description: |
408 + If the logs show `vsphere:host-no-perf-samples` or `vsphere:vm-no-perf-samples`, verify that the configured account can read vCenter performance counters for the selected hosts and VMs, and that the entities are powered on when performance metrics are expected.
409 + - name: Periodic discovery errors
410 + description: |
411 + If the logs show `vsphere:periodic-discovery-error`, check vCenter reachability, account permissions for the enabled optional surfaces, and whether the configured `timeout` is large enough for the inventory size.
412 + - name: vCenter reboot recovery
413 + description: |
414 + The collector cannot always recover an existing session after a vCenter reboot. Restart `go.d.plugin` if collection does not resume after vCenter becomes available again.
415 alerts:
416 - name: vsphere_vm_cpu_utilization
417 metric: vsphere.vm_cpu_utilization
418 info: Virtual Machine CPU utilization
419 link: https://github.com/netdata/netdata/blob/master/src/health/health.d/vsphere.conf
322 - - name: vsphere_vm_mem_usage
420 + - name: vsphere_vm_mem_utilization
421 metric: vsphere.vm_mem_utilization
422 info: Virtual Machine memory utilization
423 link: https://github.com/netdata/netdata/blob/master/src/health/health.d/vsphere.conf
424 + - name: vsphere_vm_snapshot_chain_depth
425 + metric: vsphere.vm_snapshot_max_chain_depth
426 + info: Virtual Machine snapshot maximum chain depth
427 + link: https://github.com/netdata/netdata/blob/master/src/health/health.d/vsphere.conf
428 + - name: vsphere_vm_snapshot_age
429 + metric: vsphere.vm_snapshot_max_age
430 + info: Virtual Machine oldest snapshot age
431 + link: https://github.com/netdata/netdata/blob/master/src/health/health.d/vsphere.conf
432 - name: vsphere_host_cpu_utilization
433 metric: vsphere.host_cpu_utilization
434 info: ESXi Host CPU utilization
@@ -331,6 +437,117 @@ modules:
437 metric: vsphere.host_mem_utilization
438 info: ESXi Host memory utilization
439 link: https://github.com/netdata/netdata/blob/master/src/health/health.d/vsphere.conf
440 + functions:
441 + description: |
442 + This collector exposes read-only readiness and topology functions for interactive troubleshooting in the Live tab. Both functions require selecting a configured vSphere collector job.
443 + list:
444 + - id: readiness
445 + name: vSphere Readiness
446 + description: |
447 + Reports the collector's current readiness from cached local state:
448 +
449 + - the selected vSphere collector job
450 + - whether the target URL and credentials are configured
451 + - whether the vSphere client, discovery cache, and performance-counter lists are initialized
452 + - discovered inventory counts
453 + - enabled or disabled optional metric, label, and vSAN groups
454 + - cached vSAN result counts when `collect_vsan` is enabled
455 +
456 + The function does not expose the configured vCenter URL or credentials, and it does not issue extra vCenter API calls.
457 + parameters:
458 + - id: __job
459 + name: Job
460 + description: Select which configured vSphere collector job to inspect.
461 + type: select
462 + required: true
463 + default: ""
464 + options: []
465 + returns:
466 + description: Collector readiness checks from cached local state. Each row represents one target, discovery, label, scope, metric, or vSAN readiness check.
467 + columns:
468 + - name: check
469 + type: string
470 + unit: ""
471 + description: Stable readiness check identifier.
472 + - name: scope
473 + type: string
474 + unit: ""
475 + description: Area covered by the check, such as target, discovery, labels, scope, or metrics.
476 + - name: status
477 + type: string
478 + unit: ""
479 + description: Readiness status. Possible values are ok, warning, disabled, and not_ready.
480 + - name: details
481 + type: string
482 + unit: ""
483 + description: Human-readable explanation of the current cached state for the check.
484 + performance: |
485 + Uses cached collector state only:<br/>• No additional vCenter or ESXi API requests are triggered<br/>• Response size is bounded by the number of configured optional groups and cached inventory summary rows
486 + security: |
487 + Does not expose the configured vCenter URL, username, password, or per-object inventory names:<br/>• Shows only configuration presence, resource counts, enabled feature flags, include pattern counts, and cached vSAN result counts<br/>• Access should still be restricted to authorized operators because it reveals enabled collection surfaces
488 + availability: |
489 + Available when:<br/>• The vSphere collector job is running<br/>• Returns not_ready rows while the collector is not initialized or discovery has not completed<br/>• Uses the last cached discovery and vSAN scrape state
490 + require_cloud: true
491 + - id: topology:vsphere
492 + name: vSphere Topology
493 + description: |
494 + Reports cached vSphere inventory topology for datacenters, clusters, ESXi hosts, VMs, datastores, networks, datastore clusters, and resource pools.
495 +
496 + The public topology function is `topology:vsphere`. The function builds actors and links from the selected job's cached discovery state. It does not issue extra vCenter API calls. vSphere Network and Distributed Virtual Port Group actors are included only when `collect_network_topology` is enabled.
497 + parameters:
498 + - id: __job
499 + name: Job
500 + description: Select which configured vSphere collector job provides the cached topology.
501 + type: select
502 + required: true
503 + default: ""
504 + options: []
505 + returns:
506 + description: Cached vSphere inventory topology payload. Actors represent discovered inventory objects and links represent parent-child, host-runs-VM, or host/VM-connects-network relationships.
507 + columns:
508 + - name: schema_version
509 + type: string
510 + unit: ""
511 + description: Topology payload schema version.
512 + - name: source
513 + type: string
514 + unit: ""
515 + description: Topology source identifier.
516 + - name: layer
517 + type: string
518 + unit: ""
519 + description: Topology layer identifier.
520 + - name: agent_id
521 + type: string
522 + unit: ""
523 + description: Netdata Agent identifier for the node serving the function.
524 + - name: collected_at
525 + type: datetime
526 + unit: ""
527 + description: Time when the cached topology response was built.
528 + - name: view
529 + type: string
530 + unit: ""
531 + description: Topology view identifier.
532 + - name: actors
533 + type: array
534 + unit: ""
535 + description: vSphere inventory actors, including datacenters, clusters, ESXi hosts, VMs, datastores, optional networks, datastore clusters, and resource pools.
536 + - name: links
537 + type: array
538 + unit: ""
539 + description: Topology links between vSphere inventory actors.
540 + - name: stats
541 + type: object
542 + unit: ""
543 + description: Counts of discovered inventory objects, actors, and links included in the response.
544 + performance: |
545 + Uses cached collector state only:<br/>• No additional vCenter or ESXi API requests are triggered by the function<br/>• Response size grows with discovered inventory object count<br/>• `collect_network_topology` adds Network discovery during normal collector discovery cycles when enabled
546 + security: |
547 + Exposes discovered inventory object names and status attributes already visible through vSphere chart labels and metrics:<br/>• Does not expose the configured vCenter URL, username, or password
548 + availability: |
549 + Available when:<br/>• The vSphere collector job is running<br/>• Initial discovery has completed successfully<br/>• Returns HTTP 503 while topology data is not cached yet
550 + require_cloud: true
551 metrics:
552 folding:
553 title: Metrics
@@ -338,9 +555,29 @@ modules:
555 description: ""
556 availability: []
557 scopes:
558 + - name: inventory
559 + description: These metrics refer to the discovered vSphere inventory for this collector job.
560 + labels:
561 + - name: id
562 + description: Static inventory instance ID
563 + metrics:
564 + - name: vsphere.inventory_objects
565 + description: vSphere inventory object count after include filters are applied
566 + unit: objects
567 + chart_type: line
568 + dimensions:
569 + - name: datacenters
570 + - name: folders
571 + - name: clusters
572 + - name: hosts
573 + - name: vms
574 + - name: datastores
575 + - name: resource_pools
576 - name: virtual machine
577 description: These metrics refer to the Virtual Machine.
578 labels:
579 + - name: id
580 + description: vSphere managed object reference ID
581 - name: datacenter
582 description: Datacenter name
583 - name: cluster
@@ -349,6 +586,10 @@ modules:
586 description: Host name
587 - name: vm
588 description: Virtual Machine name
589 + - name: vsphere_tag_<category>
590 + description: vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character
591 + - name: vsphere_custom_attribute_<name>
592 + description: vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys
593 metrics:
594 - name: vsphere.vm_cpu_utilization
595 description: Virtual Machine CPU utilization
@@ -413,7 +654,7 @@ modules:
654 - name: sent
655 - name: vsphere.vm_net_drops
656 description: Virtual Machine network dropped packets
416 - unit: packets
657 + unit: drops
658 chart_type: line
659 dimensions:
660 - name: received
@@ -427,21 +668,288 @@ modules:
668 - name: red
669 - name: yellow
670 - name: gray
671 + - name: vsphere.vm_power_state
672 + description: Virtual Machine power state
673 + unit: status
674 + chart_type: line
675 + dimensions:
676 + - name: powered_on
677 + - name: powered_off
678 + - name: suspended
679 + - name: vsphere.vm_connection_state
680 + description: Virtual Machine connection state
681 + unit: status
682 + chart_type: line
683 + dimensions:
684 + - name: connected
685 + - name: disconnected
686 + - name: orphaned
687 + - name: inaccessible
688 + - name: invalid
689 + - name: vsphere.vm_tools_running_status
690 + description: Virtual Machine VMware Tools running status
691 + unit: status
692 + chart_type: line
693 + dimensions:
694 + - name: running
695 + - name: not_running
696 + - name: executing_scripts
697 + - name: unknown
698 + - name: vsphere.vm_tools_version_status
699 + description: Virtual Machine VMware Tools version status
700 + unit: status
701 + chart_type: line
702 + dimensions:
703 + - name: current
704 + - name: need_upgrade
705 + - name: not_installed
706 + - name: unmanaged
707 + - name: too_old
708 + - name: supported_old
709 + - name: supported_new
710 + - name: too_new
711 + - name: blacklisted
712 + - name: unknown
713 + - name: vsphere.vm_consolidation_needed
714 + description: Virtual Machine disk consolidation status
715 + unit: status
716 + chart_type: line
717 + dimensions:
718 + - name: needed
719 + - name: not_needed
720 - name: vsphere.vm_system_uptime
721 description: Virtual Machine system uptime
722 unit: seconds
723 chart_type: line
724 dimensions:
725 - name: uptime
726 + - name: vsphere.vm_config_cpu
727 + description: Virtual Machine configured CPU
728 + unit: vCPUs
729 + chart_type: line
730 + dimensions:
731 + - name: vcpus
732 + - name: vsphere.vm_config_memory
733 + description: Virtual Machine configured memory
734 + unit: MiB
735 + chart_type: line
736 + dimensions:
737 + - name: memory
738 + - name: vsphere.vm_config_devices
739 + description: Virtual Machine configured devices
740 + unit: devices
741 + chart_type: line
742 + dimensions:
743 + - name: disks
744 + - name: nics
745 + - name: vsphere.vm_storage_usage
746 + description: Virtual Machine storage usage
747 + unit: bytes
748 + chart_type: line
749 + dimensions:
750 + - name: committed
751 + - name: uncommitted
752 + - name: unshared
753 + - name: vsphere.vm_snapshot_count
754 + description: Virtual Machine snapshot count
755 + unit: snapshots
756 + chart_type: line
757 + dimensions:
758 + - name: count
759 + - name: vsphere.vm_snapshot_max_age
760 + description: Virtual Machine oldest snapshot age; zero means no snapshots
761 + unit: seconds
762 + chart_type: line
763 + dimensions:
764 + - name: age
765 + - name: vsphere.vm_snapshot_max_chain_depth
766 + description: Virtual Machine maximum snapshot chain depth; zero means no snapshots
767 + unit: snapshots
768 + chart_type: line
769 + dimensions:
770 + - name: depth
771 + - name: virtual machine power
772 + description: These aggregate metrics refer to VM power and energy and are collected for discovered powered-on VMs when vSphere exposes the corresponding power counters.
773 + labels:
774 + - name: id
775 + description: vSphere managed object reference ID of the VM
776 + - name: datacenter
777 + description: Datacenter name
778 + - name: cluster
779 + description: Cluster name
780 + - name: host
781 + description: Host name
782 + - name: vm
783 + description: Virtual Machine name
784 + - name: vsphere_tag_<category>
785 + description: vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character
786 + - name: vsphere_custom_attribute_<name>
787 + description: vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys
788 + metrics:
789 + - name: vsphere.vm_power_usage
790 + description: Virtual Machine power usage
791 + unit: watts
792 + chart_type: line
793 + dimensions:
794 + - name: power
795 + - name: vsphere.vm_energy_usage
796 + description: Virtual Machine energy usage
797 + unit: joules
798 + chart_type: line
799 + dimensions:
800 + - name: energy
801 + - name: vSAN virtual machine
802 + description: These optional metrics refer to VM vSAN performance and are collected only when `collect_vsan` is enabled.
803 + labels:
804 + - name: id
805 + description: vSphere managed object reference ID of the VM
806 + - name: datacenter
807 + description: Datacenter name
808 + - name: cluster
809 + description: Cluster name
810 + - name: host
811 + description: Host name
812 + - name: vm
813 + description: Virtual Machine name
814 + - name: vm_instance_uuid
815 + description: VM instance UUID used by vSAN performance entity references
816 + - name: vsphere_tag_<category>
817 + description: vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character
818 + - name: vsphere_custom_attribute_<name>
819 + description: vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys
820 + metrics:
821 + - name: vsphere.vsan_vm_operations
822 + description: vSAN Virtual Machine operations
823 + unit: operations/s
824 + chart_type: line
825 + dimensions:
826 + - name: read
827 + - name: write
828 + - name: vsphere.vsan_vm_throughput
829 + description: vSAN Virtual Machine throughput
830 + unit: bytes/s
831 + chart_type: area
832 + dimensions:
833 + - name: read
834 + - name: write
835 + - name: vsphere.vsan_vm_latency
836 + description: vSAN Virtual Machine latency
837 + unit: microseconds
838 + chart_type: line
839 + dimensions:
840 + - name: read
841 + - name: write
842 + - name: host power
843 + description: These aggregate metrics refer to ESXi host power, energy, and power capacity and are collected for discovered powered-on hosts when vSphere exposes the corresponding power counters.
844 + labels:
845 + - name: id
846 + description: vSphere managed object reference ID of the host
847 + - name: datacenter
848 + description: Datacenter name
849 + - name: cluster
850 + description: Cluster name
851 + - name: host
852 + description: Host name
853 + - name: vsphere_tag_<category>
854 + description: vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character
855 + - name: vsphere_custom_attribute_<name>
856 + description: vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys
857 + metrics:
858 + - name: vsphere.host_power_usage
859 + description: ESXi Host power usage
860 + unit: watts
861 + chart_type: line
862 + dimensions:
863 + - name: power
864 + - name: cap
865 + - name: vsphere.host_power_capacity_usage
866 + description: ESXi Host power capacity usage
867 + unit: watts
868 + chart_type: line
869 + dimensions:
870 + - name: used
871 + - name: usable
872 + - name: idle
873 + - name: system
874 + - name: vm
875 + - name: vsphere.host_power_capacity_utilization
876 + description: ESXi Host power capacity utilization
877 + unit: percentage
878 + chart_type: line
879 + dimensions:
880 + - name: used
881 + - name: vsphere.host_energy_usage
882 + description: ESXi Host energy usage
883 + unit: joules
884 + chart_type: line
885 + dimensions:
886 + - name: energy
887 + - name: vSAN host
888 + description: These optional metrics refer to ESXi host vSAN performance and are collected only when `collect_vsan` is enabled.
889 + labels:
890 + - name: id
891 + description: vSphere managed object reference ID of the host
892 + - name: datacenter
893 + description: Datacenter name
894 + - name: cluster
895 + description: Cluster name
896 + - name: host
897 + description: Host name
898 + - name: vsan_node_uuid
899 + description: vSAN host node UUID used by vSAN performance entity references
900 + - name: vsphere_tag_<category>
901 + description: vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character
902 + - name: vsphere_custom_attribute_<name>
903 + description: vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys
904 + metrics:
905 + - name: vsphere.vsan_host_operations
906 + description: vSAN Host operations
907 + unit: operations/s
908 + chart_type: line
909 + dimensions:
910 + - name: read
911 + - name: write
912 + - name: vsphere.vsan_host_throughput
913 + description: vSAN Host throughput
914 + unit: bytes/s
915 + chart_type: area
916 + dimensions:
917 + - name: read
918 + - name: write
919 + - name: vsphere.vsan_host_latency
920 + description: vSAN Host latency
921 + unit: microseconds
922 + chart_type: line
923 + dimensions:
924 + - name: read
925 + - name: write
926 + - name: vsphere.vsan_host_congestions
927 + description: vSAN Host congestion events
928 + unit: congestions/s
929 + chart_type: line
930 + dimensions:
931 + - name: congestions
932 + - name: vsphere.vsan_host_cache_hit_rate
933 + description: vSAN Host client cache hit rate
934 + unit: percentage
935 + chart_type: line
936 + dimensions:
937 + - name: hit_rate
938 - name: host
939 description: These metrics refer to the ESXi host.
940 labels:
941 + - name: id
942 + description: vSphere managed object reference ID
943 - name: datacenter
944 description: Datacenter name
945 - name: cluster
946 description: Cluster name
947 - name: host
948 description: Host name
949 + - name: vsphere_tag_<category>
950 + description: vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character
951 + - name: vsphere_custom_attribute_<name>
952 + description: vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys
953 metrics:
954 - name: vsphere.host_cpu_utilization
955 description: ESXi Host CPU utilization
@@ -488,7 +996,7 @@ modules:
996 - name: vsphere.host_net_traffic
997 description: ESXi Host network traffic
998 unit: KiB/s
491 - chart_type: line
999 + chart_type: area
1000 dimensions:
1001 - name: received
1002 - name: sent
@@ -501,7 +1009,7 @@ modules:
1009 - name: sent
1010 - name: vsphere.host_net_drops
1011 description: ESXi Host network drops
504 - unit: packets
1012 + unit: drops
1013 chart_type: line
1014 dimensions:
1015 - name: received
@@ -522,6 +1030,30 @@ modules:
1030 - name: red
1031 - name: yellow
1032 - name: gray
1033 + - name: vsphere.host_power_state
1034 + description: ESXi Host power state
1035 + unit: status
1036 + chart_type: line
1037 + dimensions:
1038 + - name: powered_on
1039 + - name: powered_off
1040 + - name: standby
1041 + - name: unknown
1042 + - name: vsphere.host_connection_state
1043 + description: ESXi Host connection state
1044 + unit: status
1045 + chart_type: line
1046 + dimensions:
1047 + - name: connected
1048 + - name: not_responding
1049 + - name: disconnected
1050 + - name: vsphere.host_maintenance_status
1051 + description: ESXi Host maintenance status
1052 + unit: status
1053 + chart_type: line
1054 + dimensions:
1055 + - name: normal
1056 + - name: in_maintenance
1057 - name: vsphere.host_system_uptime
1058 description: ESXi Host system uptime
1059 unit: seconds
@@ -531,12 +1063,18 @@ modules:
1063 - name: datastore
1064 description: These metrics refer to the Datastore.
1065 labels:
1066 + - name: id
1067 + description: vSphere managed object reference ID
1068 - name: datacenter
1069 description: Datacenter name
1070 - name: datastore
1071 description: Datastore name
1072 - name: type
1073 description: "Datastore type (VMFS, NFS, NFS41, vsan, VVOL, PMEM)"
1074 + - name: vsphere_tag_<category>
1075 + description: vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character
1076 + - name: vsphere_custom_attribute_<name>
1077 + description: vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys
1078 metrics:
1079 - name: vsphere.datastore_disk_io
1080 description: Datastore disk IO
@@ -566,13 +1104,14 @@ modules:
1104 dimensions:
1105 - name: used
1106 - name: vsphere.datastore_space_usage
569 - description: Datastore space usage
1107 + description: Datastore space usage; capacity, free, used, and uncommitted are zero when the datastore is inaccessible
1108 unit: bytes
1109 chart_type: line
1110 dimensions:
1111 - name: capacity
1112 - name: free
1113 - name: used
1114 + - name: uncommitted
1115 - name: vsphere.datastore_overall_status
1116 description: Datastore overall alarm status
1117 unit: status
@@ -582,13 +1121,153 @@ modules:
1121 - name: red
1122 - name: yellow
1123 - name: gray
1124 + - name: vsphere.datastore_accessibility_status
1125 + description: Datastore accessibility status
1126 + unit: status
1127 + chart_type: line
1128 + dimensions:
1129 + - name: accessible
1130 + - name: inaccessible
1131 + - name: vsphere.datastore_maintenance_status
1132 + description: Datastore maintenance mode status
1133 + unit: status
1134 + chart_type: line
1135 + dimensions:
1136 + - name: normal
1137 + - name: entering_maintenance
1138 + - name: in_maintenance
1139 + - name: unknown
1140 + - name: vsphere.datastore_multiple_host_access
1141 + description: Datastore multi-host access status
1142 + unit: status
1143 + chart_type: line
1144 + dimensions:
1145 + - name: enabled
1146 + - name: disabled
1147 + - name: unknown
1148 + - name: datastore cluster
1149 + description: These optional metrics refer to datastore clusters (StoragePod objects) and are collected only when `collect_datastore_clusters` is enabled.
1150 + labels:
1151 + - name: id
1152 + description: vSphere managed object reference ID
1153 + - name: datacenter
1154 + description: Datacenter name
1155 + - name: datastore_cluster
1156 + description: Datastore cluster name
1157 + - name: vsphere_tag_<category>
1158 + description: vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character
1159 + - name: vsphere_custom_attribute_<name>
1160 + description: vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys
1161 + metrics:
1162 + - name: vsphere.datastore_cluster_space_utilization
1163 + description: Datastore Cluster space utilization
1164 + unit: percentage
1165 + chart_type: line
1166 + dimensions:
1167 + - name: used
1168 + - name: vsphere.datastore_cluster_space_usage
1169 + description: Datastore Cluster space usage
1170 + unit: bytes
1171 + chart_type: line
1172 + dimensions:
1173 + - name: capacity
1174 + - name: free
1175 + - name: used
1176 + - name: vsphere.datastore_cluster_storage_drs_status
1177 + description: Datastore Cluster Storage DRS status
1178 + unit: status
1179 + chart_type: line
1180 + dimensions:
1181 + - name: enabled
1182 + - name: disabled
1183 + - name: vsphere.datastore_cluster_overall_status
1184 + description: Datastore Cluster overall status
1185 + unit: status
1186 + chart_type: line
1187 + dimensions:
1188 + - name: green
1189 + - name: red
1190 + - name: yellow
1191 + - name: gray
1192 + - name: vSAN cluster
1193 + description: These optional metrics refer to vSAN cluster capacity, health, and performance and are collected only when `collect_vsan` is enabled.
1194 + labels:
1195 + - name: id
1196 + description: vSphere managed object reference ID of the cluster
1197 + - name: datacenter
1198 + description: Datacenter name
1199 + - name: cluster
1200 + description: Cluster name
1201 + - name: vsan_uuid
1202 + description: vSAN cluster UUID used by vSAN performance entity references
1203 + - name: vsphere_tag_<category>
1204 + description: vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character
1205 + - name: vsphere_custom_attribute_<name>
1206 + description: vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys
1207 + metrics:
1208 + - name: vsphere.vsan_cluster_space_usage
1209 + description: vSAN Cluster space usage
1210 + unit: bytes
1211 + chart_type: stacked
1212 + dimensions:
1213 + - name: used
1214 + - name: free
1215 + - name: total
1216 + - name: vsphere.vsan_cluster_space_utilization
1217 + description: vSAN Cluster space utilization
1218 + unit: percentage
1219 + chart_type: line
1220 + dimensions:
1221 + - name: used
1222 + - name: vsphere.vsan_cluster_health_status
1223 + description: vSAN Cluster health status
1224 + unit: status
1225 + chart_type: line
1226 + dimensions:
1227 + - name: green
1228 + - name: yellow
1229 + - name: red
1230 + - name: unknown
1231 + - name: vsphere.vsan_cluster_operations
1232 + description: vSAN Cluster operations
1233 + unit: operations/s
1234 + chart_type: line
1235 + dimensions:
1236 + - name: read
1237 + - name: write
1238 + - name: vsphere.vsan_cluster_throughput
1239 + description: vSAN Cluster throughput
1240 + unit: bytes/s
1241 + chart_type: area
1242 + dimensions:
1243 + - name: read
1244 + - name: write
1245 + - name: vsphere.vsan_cluster_latency
1246 + description: vSAN Cluster latency
1247 + unit: microseconds
1248 + chart_type: line
1249 + dimensions:
1250 + - name: read
1251 + - name: write
1252 + - name: vsphere.vsan_cluster_congestions
1253 + description: vSAN Cluster congestion events
1254 + unit: congestions/s
1255 + chart_type: line
1256 + dimensions:
1257 + - name: congestions
1258 - name: cluster
1259 description: These metrics refer to the vSphere Cluster.
1260 labels:
1261 + - name: id
1262 + description: vSphere managed object reference ID
1263 - name: datacenter
1264 description: Datacenter name
1265 - name: cluster
1266 description: Cluster name
1267 + - name: vsphere_tag_<category>
1268 + description: vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character
1269 + - name: vsphere_custom_attribute_<name>
1270 + description: vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys
1271 metrics:
1272 - name: vsphere.cluster_hosts
1273 description: Cluster host count
@@ -624,6 +1303,21 @@ modules:
1303 chart_type: line
1304 dimensions:
1305 - name: enabled
1306 + - name: vsphere.cluster_drs_mode
1307 + description: Cluster DRS automation mode
1308 + unit: status
1309 + chart_type: line
1310 + dimensions:
1311 + - name: manual
1312 + - name: partially_automated
1313 + - name: fully_automated
1314 + - name: unknown
1315 + - name: vsphere.cluster_drs_vmotion_rate
1316 + description: Cluster DRS vMotion recommendation threshold
1317 + unit: level
1318 + chart_type: line
1319 + dimensions:
1320 + - name: rate
1321 - name: vsphere.cluster_ha_config
1322 description: Cluster HA configuration
1323 unit: status
@@ -631,6 +1325,31 @@ modules:
1325 dimensions:
1326 - name: enabled
1327 - name: admission_control
1328 + - name: vsphere.cluster_ha_host_monitoring
1329 + description: Cluster HA host monitoring
1330 + unit: status
1331 + chart_type: line
1332 + dimensions:
1333 + - name: enabled
1334 + - name: disabled
1335 + - name: unknown
1336 + - name: vsphere.cluster_ha_vm_monitoring
1337 + description: Cluster HA VM monitoring
1338 + unit: status
1339 + chart_type: line
1340 + dimensions:
1341 + - name: disabled
1342 + - name: vm_monitoring_only
1343 + - name: vm_and_app_monitoring
1344 + - name: unknown
1345 + - name: vsphere.cluster_ha_vm_component_protection
1346 + description: Cluster HA VM component protection
1347 + unit: status
1348 + chart_type: line
1349 + dimensions:
1350 + - name: enabled
1351 + - name: disabled
1352 + - name: unknown
1353 - name: vsphere.cluster_overall_status
1354 description: Cluster overall alarm status
1355 unit: status
@@ -785,12 +1504,18 @@ modules:
1504 - name: resource pool
1505 description: These metrics refer to the vSphere Resource Pool.
1506 labels:
1507 + - name: id
1508 + description: vSphere managed object reference ID
1509 - name: datacenter
1510 description: Datacenter name
1511 - name: cluster
1512 description: Cluster name
1513 - name: resource_pool
1514 description: Resource Pool name
1515 + - name: vsphere_tag_<category>
1516 + description: vSphere tag label; present only for categories matched by `tag_categories`; category names are sanitized for label keys and multiple tags in one category are sorted and joined with the pipe character
1517 + - name: vsphere_custom_attribute_<name>
1518 + description: vSphere custom attribute label; present only for attributes matched by `custom_attributes`; attribute names are sanitized for label keys
1519 metrics:
1520 - name: vsphere.resource_pool_cpu_usage
1521 description: Resource Pool CPU usage vs demand
src/go/plugin/go.d/collector/vsphere/metrics.go new
+116
@@ -0,0 +1,116 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/metrix"
7 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
8 +)
9 +
10 +type collectorMetrics struct {
11 + meter metrix.SnapshotMeter
12 + gauges map[string]metrix.SnapshotGauge
13 +}
14 +
15 +const scaledPercent = 10000
16 +
17 +func newCollectorMetrics(store metrix.CollectorStore) *collectorMetrics {
18 + return &collectorMetrics{
19 + meter: store.Write().SnapshotMeter(""),
20 + gauges: make(map[string]metrix.SnapshotGauge),
21 + }
22 +}
23 +
24 +func (mx *collectorMetrics) gauge(name string) metrix.SnapshotGauge {
25 + // The gauge cache is mutated only from Collect while collectionLock is held.
26 + gauge := mx.gauges[name]
27 + if gauge == nil {
28 + gauge = mx.meter.Gauge(name)
29 + mx.gauges[name] = gauge
30 + }
31 + return gauge
32 +}
33 +
34 +func (c *Collector) observeGauge(name string, value int64, labels metrix.LabelSet) {
35 + c.observeGaugeFloat(name, float64(value), labels)
36 +}
37 +
38 +func (c *Collector) observeGaugeFloat(name string, value float64, labels metrix.LabelSet) {
39 + c.mx.gauge(name).Observe(metrix.SampleValue(value), labels)
40 +}
41 +
42 +func (c *Collector) labelSet(labels []metrix.Label) metrix.LabelSet {
43 + return c.mx.meter.LabelSet(labels...)
44 +}
45 +
46 +func (c *Collector) v2MetricLabels(id string, base []metrix.Label, enrichment map[string]string) []metrix.Label {
47 + labels := make([]metrix.Label, 0, len(base)+1)
48 + labels = append(labels, metrix.Label{Key: "id", Value: id})
49 + labels = append(labels, base...)
50 + labels = append(labels, resourceEnrichmentLabels(enrichment)...)
51 + return labels
52 +}
53 +
54 +func (c *Collector) inventoryLabelSet() metrix.LabelSet {
55 + return c.labelSet(c.v2MetricLabels("inventory", nil, nil))
56 +}
57 +
58 +func (c *Collector) hostLabelSet(host *rs.Host) metrix.LabelSet {
59 + return c.labelSet(c.v2MetricLabels(host.ID, c.hostLabels(host), host.Labels))
60 +}
61 +
62 +func (c *Collector) vmLabelSet(vm *rs.VM) metrix.LabelSet {
63 + return c.labelSet(c.v2MetricLabels(vm.ID, c.vmLabels(vm), vm.Labels))
64 +}
65 +
66 +func (c *Collector) datastoreLabelSet(ds *rs.Datastore) metrix.LabelSet {
67 + return c.labelSet(c.v2MetricLabels(ds.ID, c.datastoreLabels(ds), ds.Labels))
68 +}
69 +
70 +func (c *Collector) clusterLabelSet(cl *rs.Cluster) metrix.LabelSet {
71 + return c.labelSet(c.v2MetricLabels(cl.ID, c.clusterLabels(cl), cl.Labels))
72 +}
73 +
74 +func (c *Collector) resourcePoolLabelSet(rp *rs.ResourcePool) metrix.LabelSet {
75 + return c.labelSet(c.v2MetricLabels(rp.ID, c.resourcePoolLabels(rp), rp.Labels))
76 +}
77 +
78 +func (c *Collector) hostLabels(host *rs.Host) []metrix.Label {
79 + return []metrix.Label{
80 + {Key: "datacenter", Value: host.Hier.DC.Name},
81 + {Key: "cluster", Value: getHostClusterName(host)},
82 + {Key: "host", Value: host.Name},
83 + }
84 +}
85 +
86 +func (c *Collector) vmLabels(vm *rs.VM) []metrix.Label {
87 + return []metrix.Label{
88 + {Key: "datacenter", Value: vm.Hier.DC.Name},
89 + {Key: "cluster", Value: getVMClusterName(vm)},
90 + {Key: "host", Value: vm.Hier.Host.Name},
91 + {Key: "vm", Value: vm.Name},
92 + }
93 +}
94 +
95 +func (c *Collector) datastoreLabels(ds *rs.Datastore) []metrix.Label {
96 + return []metrix.Label{
97 + {Key: "datacenter", Value: ds.Hier.DC.Name},
98 + {Key: "datastore", Value: ds.Name},
99 + {Key: "type", Value: ds.Type},
100 + }
101 +}
102 +
103 +func (c *Collector) clusterLabels(cl *rs.Cluster) []metrix.Label {
104 + return []metrix.Label{
105 + {Key: "datacenter", Value: cl.Hier.DC.Name},
106 + {Key: "cluster", Value: cl.Name},
107 + }
108 +}
109 +
110 +func (c *Collector) resourcePoolLabels(rp *rs.ResourcePool) []metrix.Label {
111 + return []metrix.Label{
112 + {Key: "datacenter", Value: rp.Hier.DC.Name},
113 + {Key: "cluster", Value: rp.Hier.Cluster.Name},
114 + {Key: "resource_pool", Value: rp.Name},
115 + }
116 +}
src/go/plugin/go.d/collector/vsphere/metrics.txt deleted
-328
@@ -1,328 +0,0 @@
1 -// [units, statsType, hasInstance]
2 -
3 -/*
4 - virtualMachine:
5 -
6 - cpu.run.summation [ms, delta, true] [Time the virtual machine is scheduled to run]
7 - cpu.ready.summation [ms, delta, true] [Time that the virtual machine was ready, but could not get scheduled to run on the physical CPU during last measurement interval]
8 - cpu.usagemhz.average [MHz, rate, true] [CPU usage in megahertz during the interval]
9 - cpu.demandEntitlementRatio.latest [%, absolute, false] [CPU resource entitlement to CPU demand ratio (in percents)]
10 - cpu.used.summation [ms, delta, true] [Total CPU usage]
11 - cpu.idle.summation [ms, delta, true] [Total time that the CPU spent in an idle state]
12 - cpu.maxlimited.summation [ms, delta, true] [Time the virtual machine is ready to run, but is not run due to maxing out its CPU limit setting]
13 - cpu.overlap.summation [ms, delta, true] [Time the virtual machine was interrupted to perform system services on behalf of itself or other virtual machines]
14 - cpu.system.summation [ms, delta, false] [Amount of time spent on system processes on each virtual CPU in the virtual machine]
15 - cpu.demand.average [MHz, absolute, false] [The amount of CPU resources a virtual machine would use if there were no CPU contention or CPU limit]
16 - cpu.wait.summation [ms, delta, true] [Total CPU time spent in wait state]
17 - cpu.latency.average [%, rate, false] [Percent of time the virtual machine is unable to run because it is contending for access to the physical CPU(s)]
18 - cpu.costop.summation [ms, delta, true] [Time the virtual machine is ready to run, but is unable to run due to co-scheduling constraints]
19 - cpu.entitlement.latest [MHz, absolute, false] [CPU resources devoted by the ESX scheduler]
20 - cpu.readiness.average [%, rate, true] [Percentage of time that the virtual machine was ready, but could not get scheduled to run on the physical CPU]
21 - cpu.swapwait.summation [ms, delta, true] [CPU time spent waiting for swap-in]
22 - cpu.usage.average [%, rate, false] [CPU usage as a percentage during the interval]
23 -
24 - datastore.totalReadLatency.average [ms, absolute, true] [The average time a read from the datastore takes]
25 - datastore.read.average [KBps, rate, true] [Rate of reading data from the datastore]
26 - datastore.write.average [KBps, rate, true] [Rate of writing data to the datastore]
27 - datastore.maxTotalLatency.latest [ms, absolute, false] [Highest latency value across all datastores used by the host]
28 - datastore.numberWriteAveraged.average [num, rate, true] [Average number of write commands issued per second to the datastore during the collection interval]
29 - datastore.totalWriteLatency.average [ms, absolute, true] [The average time a write to the datastore takes]
30 - datastore.numberReadAveraged.average [num, rate, true] [Average number of read commands issued per second to the datastore during the collection interval]
31 -
32 - disk.read.average [KBps, rate, true] [Average number of kilobytes read from the disk each second during the collection interval]
33 - disk.commands.summation [num, delta, true] [Number of SCSI commands issued during the collection interval]
34 - disk.commandsAborted.summation [num, delta, true] [Number of SCSI commands aborted during the collection interval]
35 - disk.busResets.summation [num, delta, true] [Number of SCSI-bus reset commands issued during the collection interval]
36 - disk.maxTotalLatency.latest [ms, absolute, false] [Highest latency value across all disks used by the host]
37 - disk.write.average [KBps, rate, true] [Average number of kilobytes written to disk each second during the collection interval]
38 - disk.numberReadAveraged.average [num, rate, true] [Average number of disk reads per second during the collection interval]
39 - disk.usage.average [KBps, rate, false] [Aggregated disk I/O rate. For hosts, this metric includes the rates for all virtual machines running on the host during the collection interval.]
40 - disk.numberWrite.summation [num, delta, true] [Number of disk writes during the collection interval]
41 - disk.commandsAveraged.average [num, rate, true] [Average number of SCSI commands issued per second during the collection interval]
42 - disk.numberWriteAveraged.average [num, rate, true] [Average number of disk writes per second during the collection interval]
43 - disk.numberRead.summation [num, delta, true] [Number of disk reads during the collection interval]
44 -
45 - mem.vmmemctltarget.average [KB, absolute, false] [Desired amount of guest physical memory the balloon driver needs to reclaim, as determined by ESXi]
46 - mem.overhead.average [KB, absolute, false] [host physical memory consumed by ESXi data structures for running the virtual machines]
47 - mem.zipSaved.latest [KB, absolute, false] [host physical memory, reclaimed from a virtual machine, by memory compression. This value is less than the value of 'Compressed' memory]
48 - mem.overheadMax.average [KB, absolute, false] [host physical memory reserved by ESXi, for its data structures, for running the virtual machine]
49 - mem.consumed.average [KB, absolute, false] [Amount of host physical memory consumed for backing up guest physical memory pages]
50 - mem.overheadTouched.average [KB, absolute, false] [Estimate of the host physical memory, from Overhead consumed, that is actively read or written to by ESXi]
51 - mem.compressionRate.average [KBps, rate, false] [Rate of guest physical memory page compression by ESXi]
52 - mem.swapin.average [KB, absolute, false] [Amount of guest physical memory that is swapped in from the swap space since the virtual machine has been powered on. This value is less than or equal to the 'Swap out' counter]
53 - mem.swaptarget.average [KB, absolute, false] [Amount of memory that ESXi needs to reclaim by swapping]
54 - mem.activewrite.average [KB, absolute, false] [Amount of guest physical memory that is being actively written by guest. Activeness is estimated by ESXi]
55 - mem.decompressionRate.average [KBps, rate, false] [Rate of guest physical memory decompression]
56 - mem.entitlement.average [KB, absolute, false] [Amount of host physical memory the virtual machine deserves, as determined by ESXi]
57 - mem.swapoutRate.average [KBps, rate, false] [Rate at which guest physical memory is swapped out to the swap space]
58 - mem.swapout.average [KB, absolute, false] [Amount of guest physical memory that is swapped out from the virtual machine to its swap space since it has been powered on.]
59 - mem.shared.average [KB, absolute, false] [Amount of guest physical memory that is shared within a single virtual machine or across virtual machines]
60 - mem.compressed.average [KB, absolute, false] [Guest physical memory pages that have undergone memory compression]
61 - mem.llSwapOutRate.average [KBps, rate, false] [Rate at which guest physical memory is swapped out to the host swap cache]
62 - mem.latency.average [%, absolute, false] [Percentage of time the virtual machine spent waiting to swap in or decompress guest physical memory]
63 - mem.llSwapInRate.average [KBps, rate, false] [Rate at which guest physical memory is swapped in from the host swap cache]
64 - mem.zero.average [KB, absolute, false] [Guest physical memory pages whose content is 0x00]
65 - mem.swapinRate.average [KBps, rate, false] [Rate at which guest physical memory is swapped in from the swap space]
66 - mem.llSwapUsed.average [KB, absolute, false] [Storage space consumed on the host swap cache for storing swapped guest physical memory pages]
67 - mem.vmmemctl.average [KB, absolute, false] [Amount of guest physical memory reclaimed from the virtual machine by the balloon driver in the guest]
68 - mem.active.average [KB, absolute, false] [Amount of guest physical memory that is being actively read or written by guest. Activeness is estimated by ESXi]
69 - mem.granted.average [KB, absolute, false] [Amount of host physical memory or physical memory that is mapped for a virtual machine or a host]
70 - mem.usage.average [%, absolute, false] [Percentage of host physical memory that has been consumed]
71 - mem.zipped.latest [KB, absolute, false] [Amount of guest physical memory pages compressed by ESXi]
72 - mem.swapped.average [KB, absolute, false] [Amount of guest physical memory that is swapped out to the swap space]
73 -
74 - net.droppedTx.summation [num, delta, true] [Number of transmits dropped]
75 - net.bytesTx.average [KBps, rate, true] [Average amount of data transmitted per second]
76 - net.transmitted.average [KBps, rate, true] [Average rate at which data was transmitted during the interval]
77 - net.droppedRx.summation [num, delta, true] [Number of receives dropped]
78 - net.bytesRx.average [KBps, rate, true] [Average amount of data received per second]
79 - net.usage.average [KBps, rate, true] [Network utilization (combined transmit-rates and receive-rates) during the interval]
80 - net.multicastRx.summation [num, delta, true] [Number of multicast packets received during the sampling interval]
81 - net.broadcastTx.summation [num, delta, true] [Number of broadcast packets transmitted during the sampling interval]
82 - net.received.average [KBps, rate, true] [Average rate at which data was received during the interval]
83 - net.broadcastRx.summation [num, delta, true] [Number of broadcast packets received during the sampling interval]
84 - net.pnicBytesRx.average [KBps, rate, true] [pnicBytesRx]
85 - net.pnicBytesTx.average [KBps, rate, true] [pnicBytesTx]
86 - net.multicastTx.summation [num, delta, true] [Number of multicast packets transmitted during the sampling interval]
87 - net.packetsTx.summation [num, delta, true] [Number of packets transmitted during the interval]
88 - net.packetsRx.summation [num, delta, true] [Number of packets received during the interval]
89 -
90 - power.energy.summation [J, delta, false] [Total energy used since last stats reset]
91 - power.power.average [W, rate, false] [Current power usage]
92 -
93 - rescpu.actpk5.latest [%, absolute, false] [CPU active peak over 5 minutes]
94 - rescpu.actpk15.latest [%, absolute, false] [CPU active peak over 15 minutes]
95 - rescpu.sampleCount.latest [num, absolute, false] [Group CPU sample count]
96 - rescpu.runav15.latest [%, absolute, false] [CPU running average over 15 minutes]
97 - rescpu.actav1.latest [%, absolute, false] [CPU active average over 1 minute]
98 - rescpu.runpk1.latest [%, absolute, false] [CPU running peak over 1 minute]
99 - rescpu.actav5.latest [%, absolute, false] [CPU active average over 5 minutes]
100 - rescpu.maxLimited5.latest [%, absolute, false] [Amount of CPU resources over the limit that were refused, average over 5 minutes]
101 - rescpu.maxLimited1.latest [%, absolute, false] [Amount of CPU resources over the limit that were refused, average over 1 minute]
102 - rescpu.runav5.latest [%, absolute, false] [CPU running average over 5 minutes]
103 - rescpu.samplePeriod.latest [ms, absolute, false] [Group CPU sample period]
104 - rescpu.runpk15.latest [%, absolute, false] [CPU running peak over 15 minutes]
105 - rescpu.maxLimited15.latest [%, absolute, false] [Amount of CPU resources over the limit that were refused, average over 15 minutes]
106 - rescpu.actav15.latest [%, absolute, false] [CPU active average over 15 minutes]
107 - rescpu.runav1.latest [%, absolute, false] [CPU running average over 1 minute]
108 - rescpu.runpk5.latest [%, absolute, false] [CPU running peak over 5 minutes]
109 - rescpu.actpk1.latest [%, absolute, false] [CPU active peak over 1 minute]
110 -
111 - sys.uptime.latest [s, absolute, false] [Total time elapsed, in seconds, since last system startup]
112 - sys.heartbeat.latest [num, absolute, false] [Number of heartbeats issued per virtual machine during the interval]
113 - sys.osUptime.latest [s, absolute, false] [Total time elapsed, in seconds, since last operating system boot-up]
114 -
115 - virtualDisk.numberReadAveraged.average [num, rate, true] [Average number of read commands issued per second to the virtual disk during the collection interval]
116 - virtualDisk.largeSeeks.latest [num, absolute, true] [Number of seeks during the interval that were greater than 8192 LBNs apart]
117 - virtualDisk.readOIO.latest [num, absolute, true] [Average number of outstanding read requests to the virtual disk during the collection interval]
118 - virtualDisk.mediumSeeks.latest [num, absolute, true] [Number of seeks during the interval that were between 64 and 8192 LBNs apart]
119 - virtualDisk.write.average [KBps, rate, true] [Rate of writing data to the virtual disk]
120 - virtualDisk.smallSeeks.latest [num, absolute, true] [Number of seeks during the interval that were less than 64 LBNs apart]
121 - virtualDisk.read.average [KBps, rate, true] [Rate of reading data from the virtual disk]
122 - virtualDisk.writeLatencyUS.latest [µs, absolute, true] [Write latency in microseconds]
123 - virtualDisk.writeOIO.latest [num, absolute, true] [Average number of outstanding write requests to the virtual disk during the collection interval]
124 - virtualDisk.totalWriteLatency.average [ms, absolute, true] [The average time a write to the virtual disk takes]
125 - virtualDisk.readLoadMetric.latest [num, absolute, true] [Storage DRS virtual disk metric for the read workload model]
126 - virtualDisk.numberWriteAveraged.average [num, rate, true] [Average number of write commands issued per second to the virtual disk during the collection interval]
127 - virtualDisk.writeLoadMetric.latest [num, absolute, true] [Storage DRS virtual disk metric for the write workload model]
128 - virtualDisk.totalReadLatency.average [ms, absolute, true] [The average time a read from the virtual disk takes]
129 - virtualDisk.readIOSize.latest [num, absolute, true] [Average read request size in bytes]
130 - virtualDisk.writeIOSize.latest [num, absolute, true] [Average write request size in bytes]
131 - virtualDisk.readLatencyUS.latest [µs, absolute, true] [Read latency in microseconds]
132 -*/
133 -
134 -/*
135 - HOST:
136 -
137 - cpu.usage.average [%, rate, true] [CPU usage as a percentage during the interval]
138 - cpu.wait.summation [ms, delta, false] [Total CPU time spent in wait state]
139 - cpu.ready.summation [ms, delta, false] [Time that the virtual machine was ready, but could not get scheduled to run on the physical CPU during last measurement interval]
140 - cpu.used.summation [ms, delta, true] [Total CPU usage]
141 - cpu.demand.average [MHz, absolute, false] [The amount of CPU resources a virtual machine would use if there were no CPU contention or CPU limit]
142 - cpu.idle.summation [ms, delta, true] [Total time that the CPU spent in an idle state]
143 - cpu.latency.average [%, rate, false] [Percent of time the virtual machine is unable to run because it is contending for access to the physical CPU(s)]
144 - cpu.utilization.average [%, rate, true] [CPU utilization as a percentage during the interval (CPU usage and CPU utilization might be different due to power management technologies or hyper-threading)]
145 - cpu.coreUtilization.average [%, rate, true] [CPU utilization of the corresponding core (if hyper-threading is enabled) as a percentage during the interval (A core is utilized if either or both of its logical CPUs are utilized)]
146 - cpu.costop.summation [ms, delta, false] [Time the virtual machine is ready to run, but is unable to run due to co-scheduling constraints]
147 - cpu.totalCapacity.average [MHz, absolute, false] [Total CPU capacity reserved by and available for virtual machines]
148 - cpu.usagemhz.average [MHz, rate, false] [CPU usage in megahertz during the interval]
149 - cpu.swapwait.summation [ms, delta, false] [CPU time spent waiting for swap-in]
150 - cpu.reservedCapacity.average [MHz, absolute, false] [Total CPU capacity reserved by virtual machines]
151 - cpu.readiness.average [%, rate, false] [Percentage of time that the virtual machine was ready, but could not get scheduled to run on the physical CPU]
152 -
153 - datastore.datastoreReadLoadMetric.latest [num, absolute, true] [Storage DRS datastore metric for read workload model]
154 - datastore.datastoreNormalReadLatency.latest [num, absolute, true] [Storage DRS datastore normalized read latency]
155 - datastore.datastoreWriteLoadMetric.latest [num, absolute, true] [Storage DRS datastore metric for write workload model]
156 - datastore.datastoreMaxQueueDepth.latest [num, absolute, true] [Storage I/O Control datastore maximum queue depth]
157 - datastore.totalReadLatency.average [ms, absolute, true] [The average time a read from the datastore takes]
158 - datastore.datastoreWriteOIO.latest [num, absolute, true] [Storage DRS datastore outstanding write requests]
159 - datastore.datastoreReadIops.latest [num, absolute, true] [Storage DRS datastore read I/O rate]
160 - datastore.sizeNormalizedDatastoreLatency.average [µs, absolute, true] [Storage I/O Control size-normalized I/O latency]
161 - datastore.datastoreIops.average [num, absolute, true] [Storage I/O Control aggregated IOPS]
162 - datastore.datastoreVMObservedLatency.latest [µs, absolute, true] [The average datastore latency as seen by virtual machines]
163 - datastore.unmapIOs.summation [num, delta, true] [unmapIOs]
164 - datastore.numberWriteAveraged.average [num, rate, true] [Average number of write commands issued per second to the datastore during the collection interval]
165 - datastore.datastoreNormalWriteLatency.latest [num, absolute, true] [Storage DRS datastore normalized write latency]
166 - datastore.numberReadAveraged.average [num, rate, true] [Average number of read commands issued per second to the datastore during the collection interval]
167 - datastore.unmapSize.summation [MB, delta, true] [unmapSize]
168 - datastore.datastoreReadOIO.latest [num, absolute, true] [Storage DRS datastore outstanding read requests]
169 - datastore.write.average [KBps, rate, true] [Rate of writing data to the datastore]
170 - datastore.totalWriteLatency.average [ms, absolute, true] [The average time a write to the datastore takes]
171 - datastore.datastoreWriteIops.latest [num, absolute, true] [Storage DRS datastore write I/O rate]
172 - datastore.datastoreReadBytes.latest [num, absolute, true] [Storage DRS datastore bytes read]
173 - datastore.read.average [KBps, rate, true] [Rate of reading data from the datastore]
174 - datastore.siocActiveTimePercentage.average [%, absolute, true] [Percentage of time Storage I/O Control actively controlled datastore latency]
175 - datastore.datastoreWriteBytes.latest [num, absolute, true] [Storage DRS datastore bytes written]
176 - datastore.maxTotalLatency.latest [ms, absolute, false] [Highest latency value across all datastores used by the host]
177 -
178 - disk.queueReadLatency.average [ms, absolute, true] [Average amount of time spent in the VMkernel queue, per SCSI read command, during the collection interval]
179 - disk.numberReadAveraged.average [num, rate, true] [Average number of disk reads per second during the collection interval]
180 - disk.numberRead.summation [num, delta, true] [Number of disk reads during the collection interval]
181 - disk.queueWriteLatency.average [ms, absolute, true] [Average amount of time spent in the VMkernel queue, per SCSI write command, during the collection interval]
182 - disk.totalWriteLatency.average [ms, absolute, true] [Average amount of time taken during the collection interval to process a SCSI write command issued by the guest OS to the virtual machine]
183 - disk.kernelWriteLatency.average [ms, absolute, true] [Average amount of time, in milliseconds, spent by VMkernel to process each SCSI write command]
184 - disk.read.average [KBps, rate, true] [Average number of kilobytes read from the disk each second during the collection interval]
185 - disk.usage.average [KBps, rate, false] [Aggregated disk I/O rate. For hosts, this metric includes the rates for all virtual machines running on the host during the collection interval.]
186 - disk.kernelLatency.average [ms, absolute, true] [Average amount of time, in milliseconds, spent by VMkernel to process each SCSI command]
187 - disk.commandsAveraged.average [num, rate, true] [Average number of SCSI commands issued per second during the collection interval]
188 - disk.numberWrite.summation [num, delta, true] [Number of disk writes during the collection interval]
189 - disk.write.average [KBps, rate, true] [Average number of kilobytes written to disk each second during the collection interval]
190 - disk.queueLatency.average [ms, absolute, true] [Average amount of time spent in the VMkernel queue, per SCSI command, during the collection interval]
191 - disk.busResets.summation [num, delta, true] [Number of SCSI-bus reset commands issued during the collection interval]
192 - disk.maxTotalLatency.latest [ms, absolute, false] [Highest latency value across all disks used by the host]
193 - disk.kernelReadLatency.average [ms, absolute, true] [Average amount of time, in milliseconds, spent by VMkernel to process each SCSI read command]
194 - disk.deviceLatency.average [ms, absolute, true] [Average amount of time, in milliseconds, to complete a SCSI command from the physical device]
195 - disk.totalLatency.average [ms, absolute, true] [Average amount of time taken during the collection interval to process a SCSI command issued by the guest OS to the virtual machine]
196 - disk.commands.summation [num, delta, true] [Number of SCSI commands issued during the collection interval]
197 - disk.numberWriteAveraged.average [num, rate, true] [Average number of disk writes per second during the collection interval]
198 - disk.totalReadLatency.average [ms, absolute, true] [Average amount of time taken during the collection interval to process a SCSI read command issued from the guest OS to the virtual machine]
199 - disk.maxQueueDepth.average [num, absolute, true] [Maximum queue depth]
200 - disk.deviceWriteLatency.average [ms, absolute, true] [Average amount of time, in milliseconds, to write to the physical device]
201 - disk.commandsAborted.summation [num, delta, true] [Number of SCSI commands aborted during the collection interval]
202 - disk.deviceReadLatency.average [ms, absolute, true] [Average amount of time, in milliseconds, to read from the physical device]
203 -
204 - hbr.hbrNetRx.average [KBps, rate, false] [Average amount of data received per second]
205 - hbr.hbrNumVms.average [num, absolute, false] [Current number of replicated virtual machines]
206 - hbr.hbrNetTx.average [KBps, rate, false] [Average amount of data transmitted per second]
207 -
208 - mem.reservedCapacity.average [MB, absolute, false] [Memory reservation consumed by powered-on virtual machines]
209 - mem.swapinRate.average [KBps, rate, false] [Rate at which guest physical memory is swapped in from the swap space]
210 - mem.zero.average [KB, absolute, false] [Guest physical memory pages whose content is 0x00]
211 - mem.heapfree.average [KB, absolute, false] [Free address space in the heap of ESXi. This is less than or equal to Heap]
212 - mem.sharedcommon.average [KB, absolute, false] [Amount of host physical memory that backs shared guest physical memory (Shared)]
213 - mem.swapin.average [KB, absolute, false] [Amount of guest physical memory that is swapped in from the swap space since the virtual machine has been powered on. This value is less than or equal to the 'Swap out' counter]
214 - mem.unreserved.average [KB, absolute, false] [Amount by which reservation can be raised]
215 - mem.lowfreethreshold.average [KB, absolute, false] [Threshold of free host physical memory below which ESXi will begin actively reclaiming memory from virtual machines by swapping, compression and ballooning]
216 - mem.state.latest [num, absolute, false] [Current memory availability state of ESXi. Possible values are high, clear, soft, hard, low. The state value determines the techniques used for memory reclamation from virtual machines]
217 - mem.decompressionRate.average [KBps, rate, false] [Rate of guest physical memory decompression]
218 - mem.swapout.average [KB, absolute, false] [Amount of guest physical memory that is swapped out from the virtual machine to its swap space since it has been powered on.]
219 - mem.vmfs.pbc.capMissRatio.latest [%, absolute, false] [Trailing average of the ratio of capacity misses to compulsory misses for the VMFS PB Cache]
220 - mem.swapused.average [KB, absolute, false] [Swap storage space consumed]
221 - mem.consumed.average [KB, absolute, false] [Amount of host physical memory consumed for backing up guest physical memory pages]
222 - mem.llSwapOutRate.average [KBps, rate, false] [Rate at which guest physical memory is swapped out to the host swap cache]
223 - mem.llSwapOut.average [KB, absolute, false] [Amount of guest physical memory swapped out to the host swap cache]
224 - mem.swapoutRate.average [KBps, rate, false] [Rate at which guest physical memory is swapped out to the swap space]
225 - mem.llSwapIn.average [KB, absolute, false] [Amount of guest physical memory swapped in from host cache]
226 - mem.active.average [KB, absolute, false] [Amount of guest physical memory that is being actively read or written by guest. Activeness is estimated by ESXi]
227 - mem.latency.average [%, absolute, false] [Percentage of time the virtual machine spent waiting to swap in or decompress guest physical memory]
228 - mem.llSwapInRate.average [KBps, rate, false] [Rate at which guest physical memory is swapped in from the host swap cache]
229 - mem.vmfs.pbc.sizeMax.latest [MB, absolute, false] [Maximum size the VMFS Pointer Block Cache can grow to]
230 - mem.vmmemctl.average [KB, absolute, false] [Amount of guest physical memory reclaimed from the virtual machine by the balloon driver in the guest]
231 - mem.vmfs.pbc.size.latest [MB, absolute, false] [Space used for holding VMFS Pointer Blocks in memory]
232 - mem.overhead.average [KB, absolute, false] [host physical memory consumed by ESXi data structures for running the virtual machines]
233 - mem.vmfs.pbc.workingSet.latest [TB, absolute, false] [Amount of file blocks whose addresses are cached in the VMFS PB Cache]
234 - mem.shared.average [KB, absolute, false] [Amount of guest physical memory that is shared within a single virtual machine or across virtual machines]
235 - mem.usage.average [%, absolute, false] [Percentage of host physical memory that has been consumed]
236 - mem.vmfs.pbc.workingSetMax.latest [TB, absolute, false] [Maximum amount of file blocks whose addresses are cached in the VMFS PB Cache]
237 - mem.sysUsage.average [KB, absolute, false] [Amount of host physical memory consumed by VMkernel]
238 - mem.compressed.average [KB, absolute, false] [Guest physical memory pages that have undergone memory compression]
239 - mem.vmfs.pbc.overhead.latest [KB, absolute, false] [Amount of VMFS heap used by the VMFS PB Cache]
240 - mem.totalCapacity.average [MB, absolute, false] [Total reservation, available and consumed, for powered-on virtual machines]
241 - mem.activewrite.average [KB, absolute, false] [Amount of guest physical memory that is being actively written by guest. Activeness is estimated by ESXi]
242 - mem.granted.average [KB, absolute, false] [Amount of host physical memory or physical memory that is mapped for a virtual machine or a host]
243 - mem.compressionRate.average [KBps, rate, false] [Rate of guest physical memory page compression by ESXi]
244 - mem.heap.average [KB, absolute, false] [Virtual address space of ESXi that is dedicated to its heap]
245 - mem.llSwapUsed.average [KB, absolute, false] [Storage space consumed on the host swap cache for storing swapped guest physical memory pages]
246 -
247 - net.bytesTx.average [KBps, rate, true] [Average amount of data transmitted per second]
248 - net.droppedRx.summation [num, delta, true] [Number of receives dropped]
249 - net.transmitted.average [KBps, rate, true] [Average rate at which data was transmitted during the interval]
250 - net.multicastTx.summation [num, delta, true] [Number of multicast packets transmitted during the sampling interval]
251 - net.errorsTx.summation [num, delta, true] [Number of packets with errors transmitted during the sampling interval]
252 - net.unknownProtos.summation [num, delta, true] [Number of frames with unknown protocol received during the sampling interval]
253 - net.multicastRx.summation [num, delta, true] [Number of multicast packets received during the sampling interval]
254 - net.broadcastTx.summation [num, delta, true] [Number of broadcast packets transmitted during the sampling interval]
255 - net.received.average [KBps, rate, true] [Average rate at which data was received during the interval]
256 - net.droppedTx.summation [num, delta, true] [Number of transmits dropped]
257 - net.usage.average [KBps, rate, true] [Network utilization (combined transmit-rates and receive-rates) during the interval]
258 - net.broadcastRx.summation [num, delta, true] [Number of broadcast packets received during the sampling interval]
259 - net.packetsRx.summation [num, delta, true] [Number of packets received during the interval]
260 - net.packetsTx.summation [num, delta, true] [Number of packets transmitted during the interval]
261 - net.errorsRx.summation [num, delta, true] [Number of packets with errors received during the sampling interval]
262 - net.bytesRx.average [KBps, rate, true] [Average amount of data received per second]
263 -
264 - power.energy.summation [J, delta, false] [Total energy used since last stats reset]
265 - power.power.average [W, rate, false] [Current power usage]
266 - power.powerCap.average [W, absolute, false] [Maximum allowed power usage]
267 -
268 - rescpu.sampleCount.latest [num, absolute, false] [Group CPU sample count]
269 - rescpu.maxLimited5.latest [%, absolute, false] [Amount of CPU resources over the limit that were refused, average over 5 minutes]
270 - rescpu.runav1.latest [%, absolute, false] [CPU running average over 1 minute]
271 - rescpu.actpk5.latest [%, absolute, false] [CPU active peak over 5 minutes]
272 - rescpu.runav5.latest [%, absolute, false] [CPU running average over 5 minutes]
273 - rescpu.actav1.latest [%, absolute, false] [CPU active average over 1 minute]
274 - rescpu.runav15.latest [%, absolute, false] [CPU running average over 15 minutes]
275 - rescpu.actav15.latest [%, absolute, false] [CPU active average over 15 minutes]
276 - rescpu.actav5.latest [%, absolute, false] [CPU active average over 5 minutes]
277 - rescpu.maxLimited15.latest [%, absolute, false] [Amount of CPU resources over the limit that were refused, average over 15 minutes]
278 - rescpu.actpk1.latest [%, absolute, false] [CPU active peak over 1 minute]
279 - rescpu.runpk15.latest [%, absolute, false] [CPU running peak over 15 minutes]
280 - rescpu.samplePeriod.latest [ms, absolute, false] [Group CPU sample period]
281 - rescpu.actpk15.latest [%, absolute, false] [CPU active peak over 15 minutes]
282 - rescpu.runpk5.latest [%, absolute, false] [CPU running peak over 5 minutes]
283 - rescpu.runpk1.latest [%, absolute, false] [CPU running peak over 1 minute]
284 - rescpu.maxLimited1.latest [%, absolute, false] [Amount of CPU resources over the limit that were refused, average over 1 minute]
285 -
286 - storageAdapter.read.average [KBps, rate, true] [Rate of reading data by the storage adapter]
287 - storageAdapter.commandsAveraged.average [num, rate, true] [Average number of commands issued per second by the storage adapter during the collection interval]
288 - storageAdapter.numberWriteAveraged.average [num, rate, true] [Average number of write commands issued per second by the storage adapter during the collection interval]
289 - storageAdapter.totalWriteLatency.average [ms, absolute, true] [The average time a write by the storage adapter takes]
290 - storageAdapter.totalReadLatency.average [ms, absolute, true] [The average time a read by the storage adapter takes]
291 - storageAdapter.write.average [KBps, rate, true] [Rate of writing data by the storage adapter]
292 - storageAdapter.numberReadAveraged.average [num, rate, true] [Average number of read commands issued per second by the storage adapter during the collection interval]
293 - storageAdapter.maxTotalLatency.latest [ms, absolute, false] [Highest latency value across all storage adapters used by the host]
294 - storagePath.numberWriteAveraged.average [num, rate, true] [Average number of write commands issued per second on the storage path during the collection interval]
295 - storagePath.write.average [KBps, rate, true] [Rate of writing data on the storage path]
296 - storagePath.maxTotalLatency.latest [ms, absolute, false] [Highest latency value across all storage paths used by the host]
297 - storagePath.read.average [KBps, rate, true] [Rate of reading data on the storage path]
298 - storagePath.numberReadAveraged.average [num, rate, true] [Average number of read commands issued per second on the storage path during the collection interval]
299 - storagePath.totalWriteLatency.average [ms, absolute, true] [The average time a write issued on the storage path takes]
300 - storagePath.totalReadLatency.average [ms, absolute, true] [The average time a read issued on the storage path takes]
301 - storagePath.commandsAveraged.average [num, rate, true] [Average number of commands issued per second on the storage path during the collection interval]
302 -
303 - sys.resourceMemTouched.latest [KB, absolute, true] [Memory touched by the system resource group]
304 - sys.resourceMemSwapped.latest [KB, absolute, true] [Memory swapped out by the system resource group]
305 - sys.resourceMemShared.latest [KB, absolute, true] [Memory saved due to sharing by the system resource group]
306 - sys.resourceMemZero.latest [KB, absolute, true] [Zero filled memory used by the system resource group]
307 - sys.resourceMemMapped.latest [KB, absolute, true] [Memory mapped by the system resource group]
308 - sys.resourceCpuAllocShares.latest [num, absolute, true] [CPU allocation shares of the system resource group]
309 - sys.resourceFdUsage.latest [num, absolute, true] [Number of file descriptors used by the system resource group]
310 - sys.resourceCpuAct5.latest [%, absolute, true] [CPU active average over 5 minutes of the system resource group]
311 - sys.resourceCpuAct1.latest [%, absolute, true] [CPU active average over 1 minute of the system resource group]
312 - sys.resourceCpuUsage.average [MHz, rate, true] [Amount of CPU used by the Service Console and other applications during the interval]
313 - sys.resourceMemOverhead.latest [KB, absolute, true] [Overhead memory consumed by the system resource group]
314 - sys.resourceMemCow.latest [KB, absolute, true] [Memory shared by the system resource group]
315 - sys.resourceCpuAllocMax.latest [MHz, absolute, true] [CPU allocation limit (in MHz) of the system resource group]
316 - sys.resourceMemAllocMax.latest [KB, absolute, true] [Memory allocation limit (in KB) of the system resource group]
317 - sys.resourceMemAllocMin.latest [KB, absolute, true] [Memory allocation reservation (in KB) of the system resource group]
318 - sys.resourceCpuAllocMin.latest [MHz, absolute, true] [CPU allocation reservation (in MHz) of the system resource group]
319 - sys.resourceCpuMaxLimited1.latest [%, absolute, true] [CPU maximum limited over 1 minute of the system resource group]
320 - sys.resourceMemAllocShares.latest [num, absolute, true] [Memory allocation shares of the system resource group]
321 - sys.resourceMemConsumed.latest [KB, absolute, true] [Memory consumed by the system resource group]
322 - sys.uptime.latest [s, absolute, false] [Total time elapsed, in seconds, since last system startup]
323 - sys.resourceCpuMaxLimited5.latest [%, absolute, true] [CPU maximum limited over 5 minutes of the system resource group]
324 - sys.resourceCpuRun5.latest [%, absolute, true] [CPU running average over 5 minutes of the system resource group]
325 - sys.resourceCpuRun1.latest [%, absolute, true] [CPU running average over 1 minute of the system resource group]
326 -
327 - vflashModule.numActiveVMDKs.latest [num, absolute, true] [Number of caches controlled by the virtual flash module]
328 -*/
src/go/plugin/go.d/collector/vsphere/power_metrics.go new
+152
@@ -0,0 +1,152 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import (
6 + "github.com/vmware/govmomi/performance"
7 +
8 + "github.com/netdata/netdata/go/plugins/pkg/metrix"
9 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
10 +)
11 +
12 +const (
13 + hostPowerUsagePowerMetric = "host_power_usage_power"
14 + hostPowerUsageCapMetric = "host_power_usage_cap"
15 +
16 + hostPowerCapacityUsageUsedMetric = "host_power_capacity_usage_used"
17 + hostPowerCapacityUsageUsableMetric = "host_power_capacity_usage_usable"
18 + hostPowerCapacityUsageIdleMetric = "host_power_capacity_usage_idle"
19 + hostPowerCapacityUsageSystemMetric = "host_power_capacity_usage_system"
20 + hostPowerCapacityUsageVMMetric = "host_power_capacity_usage_vm"
21 +
22 + hostPowerCapacityUtilizationMetric = "host_power_capacity_utilization_used"
23 +
24 + hostEnergyUsageMetric = "host_energy_usage_energy"
25 +
26 + vmPowerUsagePowerMetric = "vm_power_usage_power"
27 +
28 + vmEnergyUsageMetric = "vm_energy_usage_energy"
29 +)
30 +
31 +var hostPowerMetricByCounter = map[string]string{
32 + "power.power.average": hostPowerUsagePowerMetric,
33 + "power.powerCap.average": hostPowerUsageCapMetric,
34 + "power.capacity.usage.average": hostPowerCapacityUsageUsedMetric,
35 + "power.capacity.usable.average": hostPowerCapacityUsageUsableMetric,
36 + "power.capacity.usageIdle.average": hostPowerCapacityUsageIdleMetric,
37 + "power.capacity.usageSystem.average": hostPowerCapacityUsageSystemMetric,
38 + "power.capacity.usageVm.average": hostPowerCapacityUsageVMMetric,
39 + "power.capacity.usagePct.average": hostPowerCapacityUtilizationMetric,
40 + "power.energy.summation": hostEnergyUsageMetric,
41 +}
42 +
43 +var vmPowerMetricByCounter = map[string]string{
44 + "power.power.average": vmPowerUsagePowerMetric,
45 + "power.energy.summation": vmEnergyUsageMetric,
46 +}
47 +
48 +type hostPowerPerfSample struct {
49 + host *rs.Host
50 + values map[string]int64
51 +}
52 +
53 +type vmPowerPerfSample struct {
54 + vm *rs.VM
55 + values map[string]int64
56 +}
57 +
58 +func (c *Collector) collectHostPowerMetrics(host *rs.Host, metrics []performance.MetricSeries) {
59 + for _, metric := range metrics {
60 + if len(metric.Value) == 0 || metric.Value[0] == -1 || metric.Instance != "" {
61 + continue
62 + }
63 + metricName, ok := hostPowerMetricByCounter[metric.Name]
64 + if !ok {
65 + continue
66 + }
67 + if c.hostPowerPerfSamples == nil {
68 + c.hostPowerPerfSamples = make(map[string]*hostPowerPerfSample)
69 + }
70 + sample := c.hostPowerPerfSamples[host.ID]
71 + if sample == nil {
72 + sample = &hostPowerPerfSample{
73 + host: host,
74 + values: make(map[string]int64),
75 + }
76 + c.hostPowerPerfSamples[host.ID] = sample
77 + }
78 + sample.values[metricName] = metric.Value[0]
79 + }
80 +}
81 +
82 +func (c *Collector) collectVMPowerMetrics(vm *rs.VM, metrics []performance.MetricSeries) {
83 + for _, metric := range metrics {
84 + if len(metric.Value) == 0 || metric.Value[0] == -1 || metric.Instance != "" {
85 + continue
86 + }
87 + metricName, ok := vmPowerMetricByCounter[metric.Name]
88 + if !ok {
89 + continue
90 + }
91 + if c.vmPowerPerfSamples == nil {
92 + c.vmPowerPerfSamples = make(map[string]*vmPowerPerfSample)
93 + }
94 + sample := c.vmPowerPerfSamples[vm.ID]
95 + if sample == nil {
96 + sample = &vmPowerPerfSample{
97 + vm: vm,
98 + values: make(map[string]int64),
99 + }
100 + c.vmPowerPerfSamples[vm.ID] = sample
101 + }
102 + sample.values[metricName] = metric.Value[0]
103 + }
104 +}
105 +
106 +func (c *Collector) writePowerMetrics() {
107 + c.writeHostPowerMetrics()
108 + c.writeVMPowerMetrics()
109 +}
110 +
111 +func (c *Collector) writeHostPowerMetrics() {
112 + if len(c.hostPowerPerfSamples) == 0 {
113 + return
114 + }
115 +
116 + for _, sample := range sortedHostPowerPerfSamples(c.hostPowerPerfSamples) {
117 + labels := c.labelSet(c.hostPowerLabels(sample.host))
118 + for metricName, value := range sample.values {
119 + c.observeGauge(metricName, value, labels)
120 + }
121 + }
122 +}
123 +
124 +func (c *Collector) writeVMPowerMetrics() {
125 + if len(c.vmPowerPerfSamples) == 0 {
126 + return
127 + }
128 +
129 + for _, sample := range sortedVMPowerPerfSamples(c.vmPowerPerfSamples) {
130 + labels := c.labelSet(c.vmPowerLabels(sample.vm))
131 + for metricName, value := range sample.values {
132 + c.observeGauge(metricName, value, labels)
133 + }
134 + }
135 +}
136 +
137 +func (c *Collector) hostPowerLabels(host *rs.Host) []metrix.Label {
138 + return c.v2MetricLabels(host.ID, []metrix.Label{
139 + {Key: "datacenter", Value: host.Hier.DC.Name},
140 + {Key: "cluster", Value: getHostClusterName(host)},
141 + {Key: "host", Value: host.Name},
142 + }, host.Labels)
143 +}
144 +
145 +func (c *Collector) vmPowerLabels(vm *rs.VM) []metrix.Label {
146 + return c.v2MetricLabels(vm.ID, []metrix.Label{
147 + {Key: "datacenter", Value: vm.Hier.DC.Name},
148 + {Key: "cluster", Value: getVMClusterName(vm)},
149 + {Key: "host", Value: vm.Hier.Host.Name},
150 + {Key: "vm", Value: vm.Name},
151 + }, vm.Labels)
152 +}
src/go/plugin/go.d/collector/vsphere/power_metrics_test.go new
+135
@@ -0,0 +1,135 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import (
6 + "context"
7 + "testing"
8 +
9 + "github.com/stretchr/testify/require"
10 + "github.com/vmware/govmomi/performance"
11 +
12 + "github.com/netdata/netdata/go/plugins/pkg/metrix"
13 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
14 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
15 +)
16 +
17 +func TestCollector_PowerMetricsEmitCharts(t *testing.T) {
18 + collr, _, teardown := prepareVSphereSim(t)
19 + defer teardown()
20 +
21 + require.NoError(t, collr.Init(context.Background()))
22 + host := firstSortedHost(t, collr)
23 + vm := firstSortedVM(t, collr)
24 + collr.scraper = mockPowerMetricsScraper{
25 + mockScraper: mockScraper{collr.scraper},
26 + hostID: host.ID,
27 + vmID: vm.ID,
28 + hostSeries: testHostPowerSeries(),
29 + vmSeries: testVMPowerSeries(),
30 + }
31 +
32 + require.NotEmpty(t, collectScalarSeriesForTest(t, collr))
33 +
34 + reader := collr.MetricStore().Read(metrix.ReadRaw())
35 + hostLabels := hostPowerLabelsMap(collr, host)
36 + requireMetricValue(t, reader, hostPowerUsagePowerMetric, hostLabels, 101)
37 + requireMetricValue(t, reader, hostPowerUsageCapMetric, hostLabels, 102)
38 + requireMetricValue(t, reader, hostPowerCapacityUsageUsedMetric, hostLabels, 103)
39 + requireMetricValue(t, reader, hostPowerCapacityUsageUsableMetric, hostLabels, 104)
40 + requireMetricValue(t, reader, hostPowerCapacityUsageIdleMetric, hostLabels, 105)
41 + requireMetricValue(t, reader, hostPowerCapacityUsageSystemMetric, hostLabels, 106)
42 + requireMetricValue(t, reader, hostPowerCapacityUsageVMMetric, hostLabels, 107)
43 + requireMetricValue(t, reader, hostPowerCapacityUtilizationMetric, hostLabels, 108)
44 + requireMetricValue(t, reader, hostEnergyUsageMetric, hostLabels, 109)
45 +
46 + vmLabels := vmPowerLabelsMap(collr, vm)
47 + requireMetricValue(t, reader, vmPowerUsagePowerMetric, vmLabels, 201)
48 + requireMetricValue(t, reader, vmEnergyUsageMetric, vmLabels, 202)
49 +
50 + createdCharts, createdDims := v2CreatedChartsAndDims(buildV2PlanForTest(t, collr))
51 + hostPowerChartID := findChartIDByLabelsAndContext(t, createdCharts, "vsphere.host_power_usage", map[string]string{
52 + "id": host.ID,
53 + })
54 + require.Contains(t, createdDims[hostPowerChartID], "power")
55 + require.Contains(t, createdDims[hostPowerChartID], "cap")
56 +
57 + vmPowerChartID := findChartIDByLabelsAndContext(t, createdCharts, "vsphere.vm_power_usage", map[string]string{
58 + "id": vm.ID,
59 + })
60 + require.Contains(t, createdDims[vmPowerChartID], "power")
61 + collecttest.AssertChartCoverage(t, collr, collecttest.ChartCoverageExpectation{})
62 + requireChartSelectorsMatchSeries(t, collr,
63 + "vsphere.host_power_",
64 + "vsphere.host_energy_usage",
65 + "vsphere.vm_power_usage",
66 + "vsphere.vm_energy_usage",
67 + )
68 +}
69 +
70 +type mockPowerMetricsScraper struct {
71 + mockScraper
72 + hostID string
73 + vmID string
74 + hostSeries []performance.MetricSeries
75 + vmSeries []performance.MetricSeries
76 +}
77 +
78 +func (s mockPowerMetricsScraper) ScrapeHosts(hosts rs.Hosts) []performance.EntityMetric {
79 + host := hosts.Get(s.hostID)
80 + if host == nil {
81 + return nil
82 + }
83 + return []performance.EntityMetric{{
84 + Entity: host.Ref,
85 + Value: append([]performance.MetricSeries(nil), s.hostSeries...),
86 + }}
87 +}
88 +
89 +func (s mockPowerMetricsScraper) ScrapeVMs(vms rs.VMs) []performance.EntityMetric {
90 + vm := vms.Get(s.vmID)
91 + if vm == nil {
92 + return nil
93 + }
94 + return []performance.EntityMetric{{
95 + Entity: vm.Ref,
96 + Value: append([]performance.MetricSeries(nil), s.vmSeries...),
97 + }}
98 +}
99 +
100 +func testHostPowerSeries() []performance.MetricSeries {
101 + return []performance.MetricSeries{
102 + {Name: "power.power.average", Value: []int64{101}},
103 + {Name: "power.powerCap.average", Value: []int64{102}},
104 + {Name: "power.capacity.usage.average", Value: []int64{103}},
105 + {Name: "power.capacity.usable.average", Value: []int64{104}},
106 + {Name: "power.capacity.usageIdle.average", Value: []int64{105}},
107 + {Name: "power.capacity.usageSystem.average", Value: []int64{106}},
108 + {Name: "power.capacity.usageVm.average", Value: []int64{107}},
109 + {Name: "power.capacity.usagePct.average", Value: []int64{108}},
110 + {Name: "power.energy.summation", Value: []int64{109}},
111 + }
112 +}
113 +
114 +func testVMPowerSeries() []performance.MetricSeries {
115 + return []performance.MetricSeries{
116 + {Name: "power.power.average", Value: []int64{201}},
117 + {Name: "power.energy.summation", Value: []int64{202}},
118 + }
119 +}
120 +
121 +func hostPowerLabelsMap(collr *Collector, host *rs.Host) metrix.Labels {
122 + labels := make(metrix.Labels)
123 + for _, label := range collr.hostPowerLabels(host) {
124 + labels[label.Key] = label.Value
125 + }
126 + return labels
127 +}
128 +
129 +func vmPowerLabelsMap(collr *Collector, vm *rs.VM) metrix.Labels {
130 + labels := make(metrix.Labels)
131 + for _, label := range collr.vmPowerLabels(vm) {
132 + labels[label.Key] = label.Value
133 + }
134 + return labels
135 +}
src/go/plugin/go.d/collector/vsphere/resources/resources.go
+164 -46
@@ -3,6 +3,8 @@
3 package resources
4
5 import (
6 + "time"
7 +
8 "github.com/vmware/govmomi/performance"
9 "github.com/vmware/govmomi/vim25/types"
10 )
@@ -50,6 +52,8 @@ type Resources struct {
52 Hosts Hosts
53 VMs VMs
54 Datastores Datastores
55 + Networks Networks
56 + StoragePods StoragePods
57 ResourcePools ResourcePools
58 }
59
@@ -73,27 +77,35 @@ type (
77 DC HierarchyValue
78 }
79 Cluster struct {
76 - Name string
77 - ID string
78 - ParentID string
79 - Hier ClusterHierarchy
80 - OverallStatus string
81 - NumHosts int32
82 - NumEffectiveHosts int32
83 - TotalCpu int32 // MHz
84 - TotalMemory int64 // bytes
85 - EffectiveCpu int32 // MHz
86 - EffectiveMemory int64 // MB
87 - NumCpuCores int16
88 - NumCpuThreads int16
89 - NumVmotions int32 // cumulative count
90 - DrsEnabled bool
91 - DrsMode string // fullyAutomated/partiallyAutomated/manual
92 - DrsScore int32 // 0-100, vSphere 7.0+, 0 if unavailable
93 - CurrentBalance int32 // thousandths of std dev
94 - TargetBalance int32 // thousandths of std dev
95 - HaEnabled bool
96 - HaAdmCtrlEnabled bool
80 + Name string
81 + ID string
82 + ParentID string
83 + Hier ClusterHierarchy
84 + Labels map[string]string
85 + CustomValues map[int32]string
86 + OverallStatus string
87 + NumHosts int32
88 + NumEffectiveHosts int32
89 + TotalCpu int32 // MHz
90 + TotalMemory int64 // bytes
91 + EffectiveCpu int32 // MHz
92 + EffectiveMemory int64 // MB
93 + NumCpuCores int16
94 + NumCpuThreads int16
95 + NumVmotions int32 // cumulative count
96 + DrsEnabled bool
97 + DrsMode string // fullyAutomated/partiallyAutomated/manual
98 + DrsVmotionRate int32 // 1-5 recommendation threshold
99 + DrsScore int32 // 0-100, vSphere 7.0+, 0 if unavailable
100 + CurrentBalance int32 // thousandths of std dev
101 + TargetBalance int32 // thousandths of std dev
102 + HaEnabled bool
103 + HaAdmCtrlEnabled bool
104 + HaHostMonitoring string
105 + HaVMMonitoring string
106 + HaVMComponentProtection string
107 + VSANEnabled bool
108 + VSANUUID string
109 // UsageSummary fields (nil when DRS disabled)
110 UsageCpuDemandMhz int32
111 UsageMemDemandMB int32
@@ -112,10 +124,12 @@ type (
124 Cluster HierarchyValue
125 }
126 ResourcePool struct {
115 - Name string
116 - ID string
117 - ParentID string // owner cluster ref value
118 - Hier ResourcePoolHierarchy
127 + Name string
128 + ID string
129 + ParentID string // owner cluster ref value
130 + Hier ResourcePoolHierarchy
131 + Labels map[string]string
132 + CustomValues map[int32]string
133 // QuickStats (polled via PropertyCollector)
134 OverallCpuUsage int64 // MHz
135 OverallCpuDemand int64 // MHz
@@ -129,7 +143,7 @@ type (
143 BalloonedMemory int64 // MB
144 OverheadMemory int64 // MB
145 ConsumedOverheadMemory int64 // MB
132 - CompressedMemory int64 // KB
146 + CompressedMemory int64 // KiB
147 // Runtime
148 CpuReservationUsed int64 // MHz
149 CpuMaxUsage int64 // MHz
@@ -151,13 +165,19 @@ type (
165 Cluster HierarchyValue
166 }
167 Host struct {
154 - Name string
155 - ID string
156 - ParentID string
157 - Hier HostHierarchy
158 - OverallStatus string
159 - MetricList performance.MetricList
160 - Ref types.ManagedObjectReference
168 + Name string
169 + ID string
170 + ParentID string
171 + Hier HostHierarchy
172 + Labels map[string]string
173 + CustomValues map[int32]string
174 + ConnectionState string
175 + PowerState string
176 + InMaintenanceMode bool
177 + OverallStatus string
178 + VSANNodeUUID string
179 + MetricList performance.MetricList
180 + Ref types.ManagedObjectReference
181 }
182
183 VMHierarchy struct {
@@ -167,31 +187,91 @@ type (
187 }
188
189 VM struct {
170 - Name string
171 - ID string
172 - ParentID string
173 - Hier VMHierarchy
174 - OverallStatus string
175 - MetricList performance.MetricList
176 - Ref types.ManagedObjectReference
190 + Name string
191 + ID string
192 + ParentID string
193 + FolderParentID string
194 + Hier VMHierarchy
195 + Labels map[string]string
196 + CustomValues map[int32]string
197 + ConnectionState string
198 + PowerState string
199 + ToolsRunningStatus string
200 + ToolsVersionStatus string
201 + InstanceUUID string
202 + ConsolidationNeeded bool
203 + ConfigCPU int64
204 + ConfigMemory int64
205 + ConfigDisks int64
206 + ConfigNICs int64
207 + StorageCommitted int64
208 + StorageUncommitted int64
209 + StorageUnshared int64
210 + OverallStatus string
211 + SnapshotCount int64
212 + SnapshotMaxChainDepth int64
213 + SnapshotOldestCreateTime time.Time
214 + MetricList performance.MetricList
215 + Ref types.ManagedObjectReference
216 }
217
218 DatastoreHierarchy struct {
219 DC HierarchyValue
220 }
221 Datastore struct {
222 + Name string
223 + ID string
224 + ParentID string
225 + Hier DatastoreHierarchy
226 + Labels map[string]string
227 + CustomValues map[int32]string
228 + OverallStatus string
229 + Type string // VMFS, NFS, NFS41, vsan, VVOL, PMEM
230 + Capacity int64 // bytes
231 + FreeSpace int64 // bytes
232 + Uncommitted int64 // bytes
233 + Accessible bool
234 + MaintenanceMode string
235 + MultipleHostAccess *bool
236 + MetricList performance.MetricList
237 + Ref types.ManagedObjectReference
238 + }
239 +
240 + NetworkHierarchy struct {
241 + DC HierarchyValue
242 + }
243 + Network struct {
244 Name string
245 ID string
246 + Type string
247 ParentID string
186 - Hier DatastoreHierarchy
187 - OverallStatus string
188 - Type string // VMFS, NFS, NFS41, vsan, VVOL, PMEM
189 - Capacity int64 // bytes
190 - FreeSpace int64 // bytes
248 + Hier NetworkHierarchy
249 + Labels map[string]string
250 + CustomValues map[int32]string
251 Accessible bool
192 - MetricList performance.MetricList
252 + IPPoolName string
253 + HostIDs []string
254 + VMIDs []string
255 + OverallStatus string
256 Ref types.ManagedObjectReference
257 }
258 +
259 + StoragePodHierarchy struct {
260 + DC HierarchyValue
261 + }
262 + StoragePod struct {
263 + Name string
264 + ID string
265 + ParentID string
266 + Hier StoragePodHierarchy
267 + Labels map[string]string
268 + CustomValues map[int32]string
269 + Capacity int64
270 + FreeSpace int64
271 + StorageDRSEnabled *bool
272 + OverallStatus string
273 + Ref types.ManagedObjectReference
274 + }
275 )
276
277 func (v *HierarchyValue) IsSet() bool { return v.ID != "" && v.Name != "" }
@@ -201,8 +281,38 @@ func (h ClusterHierarchy) IsSet() bool { return h.DC.IsSet() }
281 func (h HostHierarchy) IsSet() bool { return h.DC.IsSet() && h.Cluster.IsSet() }
282 func (h VMHierarchy) IsSet() bool { return h.DC.IsSet() && h.Cluster.IsSet() && h.Host.IsSet() }
283 func (h DatastoreHierarchy) IsSet() bool { return h.DC.IsSet() }
284 +func (h NetworkHierarchy) IsSet() bool { return h.DC.IsSet() }
285 +func (h StoragePodHierarchy) IsSet() bool { return h.DC.IsSet() }
286 func (h ResourcePoolHierarchy) IsSet() bool { return h.DC.IsSet() && h.Cluster.IsSet() }
287
288 +func (h *Host) IsPoweredOn() bool {
289 + return h != nil && h.PowerState == string(types.HostSystemPowerStatePoweredOn)
290 +}
291 +
292 +func (v *VM) IsPoweredOn() bool {
293 + return v != nil && v.PowerState == string(types.VirtualMachinePowerStatePoweredOn)
294 +}
295 +
296 +func SetClusterVSANInfo(cluster *Cluster, config types.BaseComputeResourceConfigInfo) {
297 + if cluster == nil {
298 + return
299 + }
300 +
301 + cluster.VSANEnabled = false
302 + cluster.VSANUUID = ""
303 +
304 + cfg, ok := config.(*types.ClusterConfigInfoEx)
305 + if !ok || cfg.VsanConfigInfo == nil {
306 + return
307 + }
308 + if cfg.VsanConfigInfo.Enabled != nil {
309 + cluster.VSANEnabled = *cfg.VsanConfigInfo.Enabled
310 + }
311 + if cfg.VsanConfigInfo.DefaultConfig != nil {
312 + cluster.VSANUUID = cfg.VsanConfigInfo.DefaultConfig.Uuid
313 + }
314 +}
315 +
316 type (
317 DataCenters map[string]*Datacenter
318 Folders map[string]*Folder
@@ -210,6 +320,8 @@ type (
320 Hosts map[string]*Host
321 VMs map[string]*VM
322 Datastores map[string]*Datastore
323 + Networks map[string]*Network
324 + StoragePods map[string]*StoragePod
325 ResourcePools map[string]*ResourcePool
326 )
327
@@ -229,6 +341,12 @@ func (vs VMs) Get(id string) *VM { return vs[id] }
341 func (ds Datastores) Put(d *Datastore) { ds[d.ID] = d }
342 func (ds Datastores) Remove(id string) { delete(ds, id) }
343 func (ds Datastores) Get(id string) *Datastore { return ds[id] }
344 +func (ns Networks) Put(n *Network) { ns[n.ID] = n }
345 +func (ns Networks) Remove(id string) { delete(ns, id) }
346 +func (ns Networks) Get(id string) *Network { return ns[id] }
347 +func (sps StoragePods) Put(sp *StoragePod) { sps[sp.ID] = sp }
348 +func (sps StoragePods) Remove(id string) { delete(sps, id) }
349 +func (sps StoragePods) Get(id string) *StoragePod { return sps[id] }
350 func (rp ResourcePools) Put(p *ResourcePool) { rp[p.ID] = p }
351 func (rp ResourcePools) Remove(id string) { delete(rp, id) }
352 func (rp ResourcePools) Get(id string) *ResourcePool { return rp[id] }
src/go/plugin/go.d/collector/vsphere/scrape/scrape.go
+51 -8
@@ -14,15 +14,22 @@ import (
14
15 "github.com/vmware/govmomi/performance"
16 "github.com/vmware/govmomi/vim25/types"
17 + vsantypes "github.com/vmware/govmomi/vsan/types"
18 )
19
20 type Client interface {
21 Version() string
22 PerformanceMetrics([]types.PerfQuerySpec) ([]performance.EntityMetric, error)
23 + VSANPerfMetrics(types.ManagedObjectReference, []vsantypes.VsanPerfQuerySpec) ([]vsantypes.VsanPerfEntityMetricCSV, error)
24 + VSANSpaceUsage(types.ManagedObjectReference) (*vsantypes.VsanSpaceUsage, error)
25 + VSANHealth(types.ManagedObjectReference) (string, error)
26 }
27
28 func New(client Client) *Scraper {
25 - v := &Scraper{Client: client}
29 + v := &Scraper{
30 + Client: client,
31 + vsanWarnings: make(map[string]bool),
32 + }
33 v.calcMaxQuery()
34 return v
35 }
@@ -30,13 +37,15 @@ func New(client Client) *Scraper {
37 type Scraper struct {
38 *logger.Logger
39 Client
33 - maxQuery int
40 + maxQuery int
41 + vsanWarnings map[string]bool
42 + vsanWarningsLock sync.Mutex
43 }
44
45 // Default settings for vCenter 6.5 and above is 256, prior versions of vCenter have this set to 64.
46 func (s *Scraper) calcMaxQuery() {
47 major, minor, err := parseVersion(s.Version())
39 - if err != nil || major < 6 || minor == 0 {
48 + if err != nil || major < 6 || (major == 6 && minor < 5) {
49 s.maxQuery = 64
50 return
51 }
@@ -112,7 +121,8 @@ func (s *Scraper) scrapeMetrics(pqs []types.PerfQuerySpec) []performance.EntityM
121 func (s *Scraper) scrape(metrics *[]performance.EntityMetric, lock *sync.Mutex, pqs []types.PerfQuerySpec) {
122 m, err := s.PerformanceMetrics(pqs)
123 if err != nil {
115 - s.Error(err)
124 + s.Limit(logKeyPerfQueryError+perfQuerySpecEntityType(pqs), 1, recurringLogEvery).
125 + Errorf("scrape vSphere performance metrics: query_specs=%d entities=[%s]: %v", len(pqs), describePerfQuerySpecs(pqs), err)
126 return
127 }
128
@@ -129,15 +139,45 @@ func chunkify(pqs []types.PerfQuerySpec, chunkSize int) (chunks [][]types.PerfQu
139 return chunks
140 }
141
142 +func describePerfQuerySpecs(pqs []types.PerfQuerySpec) string {
143 + if len(pqs) == 0 {
144 + return "none"
145 + }
146 +
147 + const limit = 5
148 + refs := make([]string, 0, min(len(pqs), limit))
149 + for _, pq := range pqs[:min(len(pqs), limit)] {
150 + refs = append(refs, fmt.Sprintf("%s/%s", pq.Entity.Type, pq.Entity.Value))
151 + }
152 + if len(pqs) > limit {
153 + refs = append(refs, fmt.Sprintf("+%d more", len(pqs)-limit))
154 + }
155 +
156 + return strings.Join(refs, ",")
157 +}
158 +
159 +func perfQuerySpecEntityType(pqs []types.PerfQuerySpec) string {
160 + if len(pqs) == 0 {
161 + return "none"
162 + }
163 + return pqs[0].Entity.Type
164 +}
165 +
166 const (
167 pqsMaxSample = 1
168 pqsIntervalID = 20
169 pqsFormat = "normal"
170 +
171 + recurringLogEvery = time.Hour
172 + logKeyPerfQueryError = "vsphere:perf-query-error:"
173 )
174
175 func newHostsPerfQuerySpecs(hosts rs.Hosts) []types.PerfQuerySpec {
176 pqs := make([]types.PerfQuerySpec, 0, len(hosts))
177 for _, host := range hosts {
178 + if !host.IsPoweredOn() || len(host.MetricList) == 0 {
179 + continue
180 + }
181 pq := types.PerfQuerySpec{
182 Entity: host.Ref,
183 MaxSample: pqsMaxSample,
@@ -153,6 +193,9 @@ func newHostsPerfQuerySpecs(hosts rs.Hosts) []types.PerfQuerySpec {
193 func newVMsPerfQuerySpecs(vms rs.VMs) []types.PerfQuerySpec {
194 pqs := make([]types.PerfQuerySpec, 0, len(vms))
195 for _, vm := range vms {
196 + if !vm.IsPoweredOn() || len(vm.MetricList) == 0 {
197 + continue
198 + }
199 pq := types.PerfQuerySpec{
200 Entity: vm.Ref,
201 MaxSample: pqsMaxSample,
@@ -172,7 +215,7 @@ const pqsHistoricalIntervalID = 300
215 func newDatastoresPerfQuerySpecs(datastores rs.Datastores) []types.PerfQuerySpec {
216 pqs := make([]types.PerfQuerySpec, 0, len(datastores))
217 for _, ds := range datastores {
175 - if len(ds.MetricList) == 0 {
218 + if !ds.Accessible || len(ds.MetricList) == 0 {
219 continue
220 }
221 pq := types.PerfQuerySpec{
@@ -208,13 +251,13 @@ func newClustersPerfQuerySpecs(clusters rs.Clusters) []types.PerfQuerySpec {
251 func parseVersion(version string) (major, minor int, err error) {
252 parts := strings.Split(version, ".")
253 if len(parts) < 2 {
211 - return 0, 0, fmt.Errorf("unparsable version string : %s", version)
254 + return 0, 0, fmt.Errorf("parse vSphere API version %q: expected <major>.<minor>", version)
255 }
256 if major, err = strconv.Atoi(parts[0]); err != nil {
214 - return 0, 0, err
257 + return 0, 0, fmt.Errorf("parse vSphere API version major component %q from %q: %w", parts[0], version, err)
258 }
259 if minor, err = strconv.Atoi(parts[1]); err != nil {
217 - return 0, 0, err
260 + return 0, 0, fmt.Errorf("parse vSphere API version minor component %q from %q: %w", parts[1], version, err)
261 }
262 return major, minor, nil
263 }
src/go/plugin/go.d/collector/vsphere/scrape/scrape_test.go
+165 -11
@@ -3,38 +3,169 @@
3 package scrape
4
5 import (
6 + "bytes"
7 "crypto/tls"
8 + "errors"
9 "net/url"
10 + "strings"
11 "testing"
12 "time"
13
14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/require"
16 + "github.com/vmware/govmomi/performance"
17 "github.com/vmware/govmomi/simulator"
18 + "github.com/vmware/govmomi/vim25/types"
19 + vsantypes "github.com/vmware/govmomi/vsan/types"
20
21 + "github.com/netdata/netdata/go/plugins/logger"
22 "github.com/netdata/netdata/go/plugins/pkg/tlscfg"
23 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/client"
24 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/discover"
25 rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
26 )
27
21 -func TestNew(t *testing.T) {
28 +func TestScraper_calcMaxQuery(t *testing.T) {
29 + tests := map[string]struct {
30 + version string
31 + want int
32 + }{
33 + "vcenter 5.5": {version: "5.5.0", want: 64},
34 + "vcenter 6.0": {version: "6.0.0", want: 64},
35 + "vcenter 6.5": {version: "6.5.0", want: 256},
36 + "vcenter 7.0": {version: "7.0.0", want: 256},
37 + "vcenter 8.0": {version: "8.0.0", want: 256},
38 + "unparsable version": {version: "not-a-version", want: 64},
39 + }
40 +
41 + for name, tt := range tests {
42 + t.Run(name, func(t *testing.T) {
43 + s := New(mockClient{version: tt.version})
44 + assert.Equal(t, tt.want, s.maxQuery)
45 + })
46 + }
47 +}
48 +
49 +func TestScraper_ScrapeInventoryPerf(t *testing.T) {
50 + tests := map[string]struct {
51 + scrape func(*Scraper, *rs.Resources) []performance.EntityMetric
52 + want func(*rs.Resources) int
53 + }{
54 + "VMs": {
55 + scrape: func(s *Scraper, res *rs.Resources) []performance.EntityMetric { return s.ScrapeVMs(res.VMs) },
56 + want: func(res *rs.Resources) int { return len(res.VMs) },
57 + },
58 + "hosts": {
59 + scrape: func(s *Scraper, res *rs.Resources) []performance.EntityMetric { return s.ScrapeHosts(res.Hosts) },
60 + want: func(res *rs.Resources) int { return len(res.Hosts) },
61 + },
62 + }
63 +
64 + for name, tc := range tests {
65 + t.Run(name, func(t *testing.T) {
66 + s, res, teardown := prepareScraper(t)
67 + defer teardown()
68 +
69 + metrics := tc.scrape(s, res)
70 + assert.Len(t, metrics, tc.want(res))
71 + })
72 + }
73 +}
74 +
75 +func TestScraper_ScrapeMetricsErrorIsRateLimited(t *testing.T) {
76 + var buf bytes.Buffer
77 + s := New(mockClient{
78 + version: "8.0.0",
79 + perfErr: errors.New("query failed"),
80 + })
81 + s.Logger = logger.NewWithWriter(&buf)
82 + hosts := rs.Hosts{
83 + "host-1": &rs.Host{
84 + ID: "host-1",
85 + PowerState: string(types.HostSystemPowerStatePoweredOn),
86 + MetricList: performance.MetricList{
87 + {CounterId: 1},
88 + },
89 + Ref: types.ManagedObjectReference{Type: "HostSystem", Value: "host-1"},
90 + },
91 + }
92 +
93 + s.ScrapeHosts(hosts)
94 + s.ScrapeHosts(hosts)
95 +
96 + assert.Equal(t, 1, strings.Count(buf.String(), "scrape vSphere performance metrics"))
97 }
98
24 -func TestScraper_ScrapeVMs(t *testing.T) {
25 - s, res, teardown := prepareScraper(t)
26 - defer teardown()
99 +func TestScraper_ScrapeVSANRecordsEmptyHealthAsUnknown(t *testing.T) {
100 + s := New(mockClient{version: "8.0.0"})
101 + clusters := rs.Clusters{
102 + "domain-c1": &rs.Cluster{
103 + ID: "domain-c1",
104 + VSANEnabled: true,
105 + VSANUUID: "cluster-uuid",
106 + Ref: types.ManagedObjectReference{Type: "ClusterComputeResource", Value: "domain-c1"},
107 + },
108 + }
109 +
110 + got := s.ScrapeVSAN(clusters, nil, nil)
111
28 - metrics := s.ScrapeVMs(res.VMs)
29 - assert.Len(t, metrics, len(res.VMs))
112 + require.Contains(t, got.Health, "domain-c1")
113 + assert.Empty(t, got.Health["domain-c1"])
114 }
115
32 -func TestScraper_ScrapeHosts(t *testing.T) {
33 - s, res, teardown := prepareScraper(t)
34 - defer teardown()
116 +func Test_newPerfQuerySpecsSkipsNonPoweredResources(t *testing.T) {
117 + tests := map[string]struct {
118 + query func() []types.PerfQuerySpec
119 + want string
120 + }{
121 + "hosts": {
122 + query: func() []types.PerfQuerySpec {
123 + return newHostsPerfQuerySpecs(rs.Hosts{
124 + "host-1": &rs.Host{
125 + ID: "host-1",
126 + PowerState: string(types.HostSystemPowerStatePoweredOn),
127 + MetricList: performance.MetricList{{CounterId: 1}},
128 + Ref: types.ManagedObjectReference{Type: "HostSystem", Value: "host-1"},
129 + },
130 + "host-2": &rs.Host{
131 + ID: "host-2",
132 + PowerState: string(types.HostSystemPowerStatePoweredOff),
133 + MetricList: performance.MetricList{{CounterId: 1}},
134 + Ref: types.ManagedObjectReference{Type: "HostSystem", Value: "host-2"},
135 + },
136 + })
137 + },
138 + want: "host-1",
139 + },
140 + "VMs": {
141 + query: func() []types.PerfQuerySpec {
142 + return newVMsPerfQuerySpecs(rs.VMs{
143 + "vm-1": &rs.VM{
144 + ID: "vm-1",
145 + PowerState: string(types.VirtualMachinePowerStatePoweredOn),
146 + MetricList: performance.MetricList{{CounterId: 1}},
147 + Ref: types.ManagedObjectReference{Type: "VirtualMachine", Value: "vm-1"},
148 + },
149 + "vm-2": &rs.VM{
150 + ID: "vm-2",
151 + PowerState: string(types.VirtualMachinePowerStateSuspended),
152 + MetricList: performance.MetricList{{CounterId: 1}},
153 + Ref: types.ManagedObjectReference{Type: "VirtualMachine", Value: "vm-2"},
154 + },
155 + })
156 + },
157 + want: "vm-1",
158 + },
159 + }
160 +
161 + for name, tc := range tests {
162 + t.Run(name, func(t *testing.T) {
163 + pqs := tc.query()
164
36 - metrics := s.ScrapeHosts(res.Hosts)
37 - assert.Len(t, metrics, len(res.Hosts))
165 + require.Len(t, pqs, 1)
166 + assert.Equal(t, tc.want, pqs[0].Entity.Value)
167 + })
168 + }
169 }
170
171 func prepareScraper(t *testing.T) (s *Scraper, res *rs.Resources, teardown func()) {
@@ -68,3 +199,26 @@ func createSim(t *testing.T) (*simulator.Model, *simulator.Server) {
199 model.Service.TLS = new(tls.Config)
200 return model, model.Service.NewServer()
201 }
202 +
203 +type mockClient struct {
204 + version string
205 + perfErr error
206 +}
207 +
208 +func (c mockClient) Version() string { return c.version }
209 +
210 +func (c mockClient) PerformanceMetrics([]types.PerfQuerySpec) ([]performance.EntityMetric, error) {
211 + return nil, c.perfErr
212 +}
213 +
214 +func (c mockClient) VSANPerfMetrics(types.ManagedObjectReference, []vsantypes.VsanPerfQuerySpec) ([]vsantypes.VsanPerfEntityMetricCSV, error) {
215 + return nil, nil
216 +}
217 +
218 +func (c mockClient) VSANSpaceUsage(types.ManagedObjectReference) (*vsantypes.VsanSpaceUsage, error) {
219 + return nil, nil
220 +}
221 +
222 +func (c mockClient) VSANHealth(types.ManagedObjectReference) (string, error) {
223 + return "", nil
224 +}
src/go/plugin/go.d/collector/vsphere/scrape/vsan.go new
+388
@@ -0,0 +1,388 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package scrape
4 +
5 +import (
6 + "errors"
7 + "reflect"
8 + "sort"
9 + "strconv"
10 + "strings"
11 + "time"
12 +
13 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
14 +
15 + "github.com/vmware/govmomi/vim25/soap"
16 + "github.com/vmware/govmomi/vim25/types"
17 + vsantypes "github.com/vmware/govmomi/vsan/types"
18 +)
19 +
20 +const (
21 + defaultVSANPerfInterval = 300
22 + maxVSANWarningKeys = 64
23 +)
24 +
25 +const (
26 + vsanQueryClusterPrefix = "cluster-domclient:"
27 + vsanQueryHostPrefix = "host-domclient:"
28 + vsanQueryVMPrefix = "virtual-machine:"
29 +)
30 +
31 +type VSANMetrics struct {
32 + Clusters map[string]VSANEntityMetrics
33 + Hosts map[string]VSANEntityMetrics
34 + VMs map[string]VSANEntityMetrics
35 + Space map[string]VSANSpaceUsage
36 + Health map[string]string
37 +}
38 +
39 +type VSANEntityMetrics map[string]float64
40 +
41 +type VSANSpaceUsage struct {
42 + Total int64
43 + Free int64
44 +}
45 +
46 +type vsanMetricSpec struct {
47 + name string
48 + rate bool
49 +}
50 +
51 +var (
52 + vsanClusterMetricSpecs = map[string]vsanMetricSpec{
53 + "iopsRead": {name: "read_operations"},
54 + "iopsWrite": {name: "write_operations"},
55 + "throughputRead": {name: "read_throughput", rate: true},
56 + "throughputWrite": {name: "write_throughput", rate: true},
57 + "latencyAvgRead": {name: "read_latency"},
58 + "latencyAvgWrite": {name: "write_latency"},
59 + "congestion": {name: "congestions", rate: true},
60 + }
61 + vsanHostMetricSpecs = map[string]vsanMetricSpec{
62 + "iopsRead": {name: "read_operations"},
63 + "iopsWrite": {name: "write_operations"},
64 + "throughputRead": {name: "read_throughput", rate: true},
65 + "throughputWrite": {name: "write_throughput", rate: true},
66 + "latencyAvgRead": {name: "read_latency"},
67 + "latencyAvgWrite": {name: "write_latency"},
68 + "congestion": {name: "congestions", rate: true},
69 + "clientCacheHitRate": {name: "cache_hit_rate"},
70 + }
71 + vsanVMMetricSpecs = map[string]vsanMetricSpec{
72 + "iopsRead": {name: "read_operations"},
73 + "iopsWrite": {name: "write_operations"},
74 + "throughputRead": {name: "read_throughput", rate: true},
75 + "throughputWrite": {name: "write_throughput", rate: true},
76 + "latencyRead": {name: "read_latency"},
77 + "latencyWrite": {name: "write_latency"},
78 + }
79 +)
80 +
81 +func (s *Scraper) ScrapeVSAN(clusters rs.Clusters, hosts rs.Hosts, vms rs.VMs) *VSANMetrics {
82 + out := &VSANMetrics{
83 + Clusters: make(map[string]VSANEntityMetrics),
84 + Hosts: make(map[string]VSANEntityMetrics),
85 + VMs: make(map[string]VSANEntityMetrics),
86 + Space: make(map[string]VSANSpaceUsage),
87 + Health: make(map[string]string),
88 + }
89 +
90 + vsanClusters := sortedVSANClusters(clusters)
91 + if len(vsanClusters) == 0 {
92 + return out
93 + }
94 +
95 + clusterByUUID := clusterIDByVSANUUID(clusters)
96 + hostByUUID := hostIDByVSANNodeUUID(hosts)
97 + vmByUUID := vmIDByInstanceUUID(vms)
98 +
99 + for _, cluster := range vsanClusters {
100 + s.scrapeVSANClusterSummary(out, cluster)
101 + s.scrapeVSANPerf(out.Clusters, cluster.Ref, vsanClusterQueryIDs(cluster), vsanClusterMetricSpecs, clusterByUUID)
102 + s.scrapeVSANPerf(out.Hosts, cluster.Ref, vsanHostQueryIDs(cluster, hosts), vsanHostMetricSpecs, hostByUUID)
103 + s.scrapeVSANPerf(out.VMs, cluster.Ref, vsanVMQueryIDs(cluster, vms), vsanVMMetricSpecs, vmByUUID)
104 + }
105 +
106 + return out
107 +}
108 +
109 +func (s *Scraper) scrapeVSANClusterSummary(out *VSANMetrics, cluster *rs.Cluster) {
110 + space, err := s.VSANSpaceUsage(cluster.Ref)
111 + if err != nil {
112 + s.warnVSANOnce("space:"+vsanFaultName(err), "failed to query vSAN space usage for cluster %s: %v", cluster.ID, err)
113 + } else if space != nil {
114 + out.Space[cluster.ID] = VSANSpaceUsage{
115 + Total: space.TotalCapacityB,
116 + Free: space.FreeCapacityB,
117 + }
118 + }
119 +
120 + health, err := s.VSANHealth(cluster.Ref)
121 + if err != nil {
122 + s.warnVSANOnce("health:"+vsanFaultName(err), "failed to query vSAN health for cluster %s: %v", cluster.ID, err)
123 + return
124 + }
125 + out.Health[cluster.ID] = health
126 +}
127 +
128 +func (s *Scraper) scrapeVSANPerf(dst map[string]VSANEntityMetrics, cluster types.ManagedObjectReference, queries []string, specs map[string]vsanMetricSpec, idByUUID map[string]string) {
129 + if len(queries) == 0 {
130 + return
131 + }
132 + now := time.Now()
133 + start := now.Add(-defaultVSANPerfInterval * time.Second)
134 + qspecs := make([]vsantypes.VsanPerfQuerySpec, 0, len(queries))
135 + labels := sortedVSANMetricLabels(specs)
136 + for _, query := range queries {
137 + qspecs = append(qspecs, vsantypes.VsanPerfQuerySpec{
138 + EntityRefId: query,
139 + StartTime: &start,
140 + EndTime: &now,
141 + Labels: labels,
142 + })
143 + }
144 + raw, err := s.VSANPerfMetrics(cluster, qspecs)
145 + if err != nil {
146 + s.warnVSANOnce("perf:"+queries[0]+":"+vsanFaultName(err), "failed to query %d vSAN performance entity refs for cluster %s: %v", len(queries), cluster.Value, err)
147 + return
148 + }
149 +
150 + values, err := parseVSANEntityMetrics(raw, specs)
151 + if err != nil {
152 + s.warnVSANOnce("parse:"+queries[0], "failed to parse vSAN performance metrics for cluster %s: %v", cluster.Value, err)
153 + return
154 + }
155 +
156 + for uuid, metrics := range values {
157 + id := idByUUID[uuid]
158 + if id == "" || len(metrics) == 0 {
159 + continue
160 + }
161 + dst[id] = metrics
162 + }
163 +}
164 +
165 +func parseVSANEntityMetrics(raw []vsantypes.VsanPerfEntityMetricCSV, specs map[string]vsanMetricSpec) (map[string]VSANEntityMetrics, error) {
166 + out := make(map[string]VSANEntityMetrics)
167 + for _, entity := range raw {
168 + uuid, ok := vsanEntityUUID(entity.EntityRefId)
169 + if !ok {
170 + continue
171 + }
172 + values := make(VSANEntityMetrics)
173 + sampleIndex := latestVSANSampleIndex(entity)
174 + for _, series := range entity.Value {
175 + spec, ok := specs[series.MetricId.Label]
176 + if !ok {
177 + continue
178 + }
179 + value, ok := latestVSANValue(series.Values, sampleIndex)
180 + if !ok {
181 + continue
182 + }
183 + if spec.rate {
184 + interval := series.MetricId.MetricsCollectInterval
185 + if interval == 0 {
186 + interval = defaultVSANPerfInterval
187 + }
188 + value /= float64(interval)
189 + }
190 + values[spec.name] = value
191 + }
192 + if len(values) > 0 {
193 + out[uuid] = values
194 + }
195 + }
196 + return out, nil
197 +}
198 +
199 +func latestVSANSampleIndex(entity vsantypes.VsanPerfEntityMetricCSV) int {
200 + sampleCount := len(csvParts(entity.SampleInfo))
201 + if sampleCount == 0 {
202 + return -1
203 + }
204 + latest := -1
205 + for _, series := range entity.Value {
206 + parts := csvParts(series.Values)
207 + limit := len(parts) - 1
208 + if sampleCount > 0 && limit >= sampleCount {
209 + limit = sampleCount - 1
210 + }
211 + for i := limit; i >= 0; i-- {
212 + if strings.TrimSpace(parts[i]) == "" {
213 + continue
214 + }
215 + if i > latest {
216 + latest = i
217 + }
218 + break
219 + }
220 + }
221 + return latest
222 +}
223 +
224 +func latestVSANValue(csv string, sampleIndex int) (float64, bool) {
225 + parts := csvParts(csv)
226 + if sampleIndex >= 0 {
227 + if sampleIndex >= len(parts) {
228 + return 0, false
229 + }
230 + part := strings.TrimSpace(parts[sampleIndex])
231 + if part == "" {
232 + return 0, false
233 + }
234 + v, err := strconv.ParseFloat(part, 64)
235 + return v, err == nil
236 + }
237 +
238 + return latestNonEmptyVSANValue(parts)
239 +}
240 +
241 +func latestNonEmptyVSANValue(parts []string) (float64, bool) {
242 + if len(parts) == 0 {
243 + return 0, false
244 + }
245 + for i := len(parts) - 1; i >= 0; i-- {
246 + part := strings.TrimSpace(parts[i])
247 + if part == "" {
248 + continue
249 + }
250 + v, err := strconv.ParseFloat(part, 64)
251 + return v, err == nil
252 + }
253 + return 0, false
254 +}
255 +
256 +func csvParts(csv string) []string {
257 + if csv == "" {
258 + return nil
259 + }
260 + return strings.Split(csv, ",")
261 +}
262 +
263 +func vsanEntityUUID(refID string) (string, bool) {
264 + _, uuid, ok := strings.Cut(refID, ":")
265 + return uuid, ok && uuid != ""
266 +}
267 +
268 +func sortedVSANMetricLabels(specs map[string]vsanMetricSpec) []string {
269 + labels := make([]string, 0, len(specs))
270 + for label := range specs {
271 + labels = append(labels, label)
272 + }
273 + sort.Strings(labels)
274 + return labels
275 +}
276 +
277 +func sortedVSANClusters(clusters rs.Clusters) []*rs.Cluster {
278 + out := make([]*rs.Cluster, 0, len(clusters))
279 + for _, cluster := range clusters {
280 + if cluster.VSANEnabled {
281 + out = append(out, cluster)
282 + }
283 + }
284 + sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
285 + return out
286 +}
287 +
288 +func vsanClusterQueryIDs(cluster *rs.Cluster) []string {
289 + if cluster == nil || cluster.VSANUUID == "" {
290 + return nil
291 + }
292 + return []string{vsanQueryClusterPrefix + cluster.VSANUUID}
293 +}
294 +
295 +func vsanHostQueryIDs(cluster *rs.Cluster, hosts rs.Hosts) []string {
296 + if cluster == nil {
297 + return nil
298 + }
299 + var out []string
300 + for _, host := range hosts {
301 + if host.Hier.Cluster.ID == cluster.ID && host.VSANNodeUUID != "" {
302 + out = append(out, vsanQueryHostPrefix+host.VSANNodeUUID)
303 + }
304 + }
305 + sort.Strings(out)
306 + return out
307 +}
308 +
309 +func vsanVMQueryIDs(cluster *rs.Cluster, vms rs.VMs) []string {
310 + if cluster == nil {
311 + return nil
312 + }
313 + var out []string
314 + for _, vm := range vms {
315 + if vm.Hier.Cluster.ID == cluster.ID && vm.InstanceUUID != "" {
316 + out = append(out, vsanQueryVMPrefix+vm.InstanceUUID)
317 + }
318 + }
319 + sort.Strings(out)
320 + return out
321 +}
322 +
323 +func clusterIDByVSANUUID(clusters rs.Clusters) map[string]string {
324 + out := make(map[string]string, len(clusters))
325 + for _, cluster := range clusters {
326 + if cluster.VSANUUID != "" {
327 + out[cluster.VSANUUID] = cluster.ID
328 + }
329 + }
330 + return out
331 +}
332 +
333 +func hostIDByVSANNodeUUID(hosts rs.Hosts) map[string]string {
334 + out := make(map[string]string, len(hosts))
335 + for _, host := range hosts {
336 + if host.VSANNodeUUID != "" {
337 + out[host.VSANNodeUUID] = host.ID
338 + }
339 + }
340 + return out
341 +}
342 +
343 +func vmIDByInstanceUUID(vms rs.VMs) map[string]string {
344 + out := make(map[string]string, len(vms))
345 + for _, vm := range vms {
346 + if vm.InstanceUUID != "" {
347 + out[vm.InstanceUUID] = vm.ID
348 + }
349 + }
350 + return out
351 +}
352 +
353 +func (s *Scraper) warnVSANOnce(key, format string, args ...any) {
354 + s.vsanWarningsLock.Lock()
355 + defer s.vsanWarningsLock.Unlock()
356 +
357 + if s.vsanWarnings == nil {
358 + s.vsanWarnings = make(map[string]bool)
359 + }
360 + if s.vsanWarnings[key] {
361 + return
362 + }
363 + if len(s.vsanWarnings) >= maxVSANWarningKeys {
364 + return
365 + }
366 + s.vsanWarnings[key] = true
367 + s.Warningf(format, args...)
368 +}
369 +
370 +func vsanFaultName(err error) string {
371 + for e := err; e != nil; e = errors.Unwrap(e) {
372 + if !soap.IsSoapFault(e) {
373 + continue
374 + }
375 + fault := soap.ToSoapFault(e)
376 + if fault.Detail.Fault != nil {
377 + t := reflect.TypeOf(fault.Detail.Fault)
378 + if t.Kind() == reflect.Pointer {
379 + t = t.Elem()
380 + }
381 + return t.Name()
382 + }
383 + if fault.String != "" {
384 + return fault.String
385 + }
386 + }
387 + return "unknown"
388 +}
src/go/plugin/go.d/collector/vsphere/scrape/vsan_test.go new
+204
@@ -0,0 +1,204 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package scrape
4 +
5 +import (
6 + "testing"
7 +
8 + "github.com/stretchr/testify/require"
9 + "github.com/vmware/govmomi/vim25/types"
10 + vsantypes "github.com/vmware/govmomi/vsan/types"
11 +
12 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
13 +)
14 +
15 +func TestParseVSANEntityMetrics(t *testing.T) {
16 + tests := map[string]struct {
17 + raw []vsantypes.VsanPerfEntityMetricCSV
18 + specs map[string]vsanMetricSpec
19 + want map[string]VSANEntityMetrics
20 + }{
21 + "cluster latest values and rates": {
22 + raw: []vsantypes.VsanPerfEntityMetricCSV{{
23 + EntityRefId: "cluster-domclient:cluster-uuid",
24 + Value: []vsantypes.VsanPerfMetricSeriesCSV{
25 + {
26 + MetricId: vsantypes.VsanPerfMetricId{Label: "iopsRead"},
27 + Values: "10,20",
28 + },
29 + {
30 + MetricId: vsantypes.VsanPerfMetricId{Label: "throughputRead", MetricsCollectInterval: 20},
31 + Values: "100,200",
32 + },
33 + {
34 + MetricId: vsantypes.VsanPerfMetricId{Label: "congestion"},
35 + Values: "300",
36 + },
37 + {
38 + MetricId: vsantypes.VsanPerfMetricId{Label: "ignored"},
39 + Values: "999",
40 + },
41 + },
42 + }},
43 + want: map[string]VSANEntityMetrics{
44 + "cluster-uuid": {
45 + "read_operations": 20,
46 + "read_throughput": 10,
47 + "congestions": 1,
48 + },
49 + },
50 + },
51 + "host label set follows vSAN API labels": {
52 + specs: vsanHostMetricSpecs,
53 + raw: []vsantypes.VsanPerfEntityMetricCSV{{
54 + EntityRefId: "host-domclient:host-uuid",
55 + Value: []vsantypes.VsanPerfMetricSeriesCSV{
56 + {
57 + MetricId: vsantypes.VsanPerfMetricId{Label: "throughputRead", MetricsCollectInterval: 300},
58 + Values: "600",
59 + },
60 + {
61 + MetricId: vsantypes.VsanPerfMetricId{Label: "latencyAvgRead"},
62 + Values: "700",
63 + },
64 + {
65 + MetricId: vsantypes.VsanPerfMetricId{Label: "clientCacheHitRate"},
66 + Values: "85",
67 + },
68 + {
69 + MetricId: vsantypes.VsanPerfMetricId{Label: "congestion", MetricsCollectInterval: 300},
70 + Values: "900",
71 + },
72 + },
73 + }},
74 + want: map[string]VSANEntityMetrics{
75 + "host-uuid": {
76 + "read_throughput": 2,
77 + "read_latency": 700,
78 + "cache_hit_rate": 85,
79 + "congestions": 3,
80 + },
81 + },
82 + },
83 + "vm label set uses latencyRead latencyWrite": {
84 + specs: vsanVMMetricSpecs,
85 + raw: []vsantypes.VsanPerfEntityMetricCSV{{
86 + EntityRefId: "virtual-machine:vm-uuid",
87 + Value: []vsantypes.VsanPerfMetricSeriesCSV{
88 + {
89 + MetricId: vsantypes.VsanPerfMetricId{Label: "throughputWrite", MetricsCollectInterval: 300},
90 + Values: "1200",
91 + },
92 + {
93 + MetricId: vsantypes.VsanPerfMetricId{Label: "latencyRead"},
94 + Values: "11",
95 + },
96 + {
97 + MetricId: vsantypes.VsanPerfMetricId{Label: "latencyWrite"},
98 + Values: "12",
99 + },
100 + {
101 + MetricId: vsantypes.VsanPerfMetricId{Label: "latencyAvgRead"},
102 + Values: "13",
103 + },
104 + },
105 + }},
106 + want: map[string]VSANEntityMetrics{
107 + "vm-uuid": {
108 + "write_throughput": 4,
109 + "read_latency": 11,
110 + "write_latency": 12,
111 + },
112 + },
113 + },
114 + "sample info aligns series to the latest sample bucket": {
115 + raw: []vsantypes.VsanPerfEntityMetricCSV{{
116 + EntityRefId: "cluster-domclient:cluster-uuid",
117 + SampleInfo: "2026-05-09 10:00:00,2026-05-09 10:05:00",
118 + Value: []vsantypes.VsanPerfMetricSeriesCSV{
119 + {
120 + MetricId: vsantypes.VsanPerfMetricId{Label: "iopsRead"},
121 + Values: "20,",
122 + },
123 + {
124 + MetricId: vsantypes.VsanPerfMetricId{Label: "throughputRead", MetricsCollectInterval: 20},
125 + Values: "100,200",
126 + },
127 + },
128 + }},
129 + want: map[string]VSANEntityMetrics{
130 + "cluster-uuid": {
131 + "read_throughput": 10,
132 + },
133 + },
134 + },
135 + "empty value skipped": {
136 + raw: []vsantypes.VsanPerfEntityMetricCSV{{
137 + EntityRefId: "host-domclient:host-uuid",
138 + Value: []vsantypes.VsanPerfMetricSeriesCSV{{
139 + MetricId: vsantypes.VsanPerfMetricId{Label: "iopsRead"},
140 + Values: "",
141 + }},
142 + }},
143 + want: map[string]VSANEntityMetrics{},
144 + },
145 + }
146 +
147 + for name, tc := range tests {
148 + t.Run(name, func(t *testing.T) {
149 + specs := tc.specs
150 + if specs == nil {
151 + specs = vsanClusterMetricSpecs
152 + }
153 + got, err := parseVSANEntityMetrics(tc.raw, specs)
154 + require.NoError(t, err)
155 + require.Equal(t, tc.want, got)
156 + })
157 + }
158 +}
159 +
160 +func TestParseVSANEntityMetricsSkipsEntityWithoutUUID(t *testing.T) {
161 + got, err := parseVSANEntityMetrics([]vsantypes.VsanPerfEntityMetricCSV{
162 + {
163 + EntityRefId: "cluster-domclient",
164 + Value: []vsantypes.VsanPerfMetricSeriesCSV{{
165 + MetricId: vsantypes.VsanPerfMetricId{Label: "iopsRead"},
166 + Values: "1",
167 + }},
168 + },
169 + {
170 + EntityRefId: "cluster-domclient:cluster-uuid",
171 + Value: []vsantypes.VsanPerfMetricSeriesCSV{{
172 + MetricId: vsantypes.VsanPerfMetricId{Label: "iopsRead"},
173 + Values: "2",
174 + }},
175 + },
176 + }, vsanClusterMetricSpecs)
177 +
178 + require.NoError(t, err)
179 + require.Equal(t, map[string]VSANEntityMetrics{
180 + "cluster-uuid": {"read_operations": 2},
181 + }, got)
182 +}
183 +
184 +func TestVSANQueryIDsUseConcreteDiscoveredEntities(t *testing.T) {
185 + cluster := &rs.Cluster{
186 + ID: "domain-c1",
187 + VSANUUID: "cluster-uuid",
188 + Ref: types.ManagedObjectReference{Type: "ClusterComputeResource", Value: "domain-c1"},
189 + }
190 + hosts := rs.Hosts{
191 + "host-1": {ID: "host-1", VSANNodeUUID: "node-2", Hier: rs.HostHierarchy{Cluster: rs.HierarchyValue{ID: "domain-c1"}}},
192 + "host-2": {ID: "host-2", VSANNodeUUID: "node-1", Hier: rs.HostHierarchy{Cluster: rs.HierarchyValue{ID: "domain-c1"}}},
193 + "host-3": {ID: "host-3", VSANNodeUUID: "node-3", Hier: rs.HostHierarchy{Cluster: rs.HierarchyValue{ID: "domain-c2"}}},
194 + }
195 + vms := rs.VMs{
196 + "vm-1": {ID: "vm-1", InstanceUUID: "vm-2", Hier: rs.VMHierarchy{Cluster: rs.HierarchyValue{ID: "domain-c1"}}},
197 + "vm-2": {ID: "vm-2", InstanceUUID: "vm-1", Hier: rs.VMHierarchy{Cluster: rs.HierarchyValue{ID: "domain-c1"}}},
198 + "vm-3": {ID: "vm-3", InstanceUUID: "vm-3", Hier: rs.VMHierarchy{Cluster: rs.HierarchyValue{ID: "domain-c2"}}},
199 + }
200 +
201 + require.Equal(t, []string{"cluster-domclient:cluster-uuid"}, vsanClusterQueryIDs(cluster))
202 + require.Equal(t, []string{"host-domclient:node-1", "host-domclient:node-2"}, vsanHostQueryIDs(cluster, hosts))
203 + require.Equal(t, []string{"virtual-machine:vm-1", "virtual-machine:vm-2"}, vsanVMQueryIDs(cluster, vms))
204 +}
src/go/plugin/go.d/collector/vsphere/sort.go new
+67
@@ -0,0 +1,67 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import (
6 + "sort"
7 +
8 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
9 +)
10 +
11 +func sortedDatacenters(in rs.DataCenters) []*rs.Datacenter {
12 + return sortedValuesByID(in, func(v *rs.Datacenter) string { return v.ID })
13 +}
14 +
15 +func sortedClusters(in rs.Clusters) []*rs.Cluster {
16 + return sortedValuesByID(in, func(v *rs.Cluster) string { return v.ID })
17 +}
18 +
19 +func sortedHosts(in rs.Hosts) []*rs.Host {
20 + return sortedValuesByID(in, func(v *rs.Host) string { return v.ID })
21 +}
22 +
23 +func sortedVMs(in rs.VMs) []*rs.VM {
24 + return sortedValuesByID(in, func(v *rs.VM) string { return v.ID })
25 +}
26 +
27 +func sortedDatastores(in rs.Datastores) []*rs.Datastore {
28 + return sortedValuesByID(in, func(v *rs.Datastore) string { return v.ID })
29 +}
30 +
31 +func sortedNetworks(in rs.Networks) []*rs.Network {
32 + return sortedValuesByID(in, func(v *rs.Network) string { return v.ID })
33 +}
34 +
35 +func sortedStoragePods(in rs.StoragePods) []*rs.StoragePod {
36 + return sortedValuesByID(in, func(v *rs.StoragePod) string { return v.ID })
37 +}
38 +
39 +func sortedResourcePools(in rs.ResourcePools) []*rs.ResourcePool {
40 + return sortedValuesByID(in, func(v *rs.ResourcePool) string { return v.ID })
41 +}
42 +
43 +func sortedHostPowerPerfSamples(samples map[string]*hostPowerPerfSample) []*hostPowerPerfSample {
44 + return sortedValuesByID(samples, func(v *hostPowerPerfSample) string { return v.host.ID })
45 +}
46 +
47 +func sortedVMPowerPerfSamples(samples map[string]*vmPowerPerfSample) []*vmPowerPerfSample {
48 + return sortedValuesByID(samples, func(v *vmPowerPerfSample) string { return v.vm.ID })
49 +}
50 +
51 +func sortedValuesByID[M ~map[string]V, V any](in M, id func(V) string) []V {
52 + out := make([]V, 0, len(in))
53 + for _, v := range in {
54 + out = append(out, v)
55 + }
56 + sort.Slice(out, func(i, j int) bool { return id(out[i]) < id(out[j]) })
57 + return out
58 +}
59 +
60 +func sortedMapKeys[V any](m map[string]V) []string {
61 + keys := make([]string, 0, len(m))
62 + for k := range m {
63 + keys = append(keys, k)
64 + }
65 + sort.Strings(keys)
66 + return keys
67 +}
src/go/plugin/go.d/collector/vsphere/task.go
+4
@@ -42,6 +42,10 @@ func (t *task) stop() {
42 t.once.Do(func() { close(t.done) })
43 }
44
45 +func (t *task) wait() {
46 + <-t.running
47 +}
48 +
49 func (t *task) isStopped() bool {
50 select {
51 case <-t.done:
src/go/plugin/go.d/collector/vsphere/task_test.go
+35 -18
@@ -16,26 +16,43 @@ func Test_task(t *testing.T) {
16 atomic.AddInt64(&i, 1)
17 }
18
19 - task := newTask(job, time.Millisecond*200)
20 - defer task.stop()
21 - time.Sleep(time.Second)
22 - assert.True(t, atomic.LoadInt64(&i) > 0)
19 + task := newTask(job, time.Millisecond)
20 + defer func() {
21 + task.stop()
22 + task.wait()
23 + }()
24 +
25 + assert.Eventually(t, func() bool {
26 + return atomic.LoadInt64(&i) > 0
27 + }, time.Second, time.Millisecond)
28 }
29
25 -func Test_task_isStopped(t *testing.T) {
26 - task := newTask(func() {}, time.Second)
27 - assert.False(t, task.isStopped())
28 -
29 - task.stop()
30 - time.Sleep(time.Millisecond * 500)
31 - assert.True(t, task.isStopped())
32 -}
30 +func Test_task_state(t *testing.T) {
31 + tests := map[string]struct {
32 + state func(*task) bool
33 + wantBefore bool
34 + wantAfter bool
35 + }{
36 + "is stopped": {
37 + state: (*task).isStopped,
38 + wantBefore: false,
39 + wantAfter: true,
40 + },
41 + "is running": {
42 + state: (*task).isRunning,
43 + wantBefore: true,
44 + wantAfter: false,
45 + },
46 + }
47
34 -func Test_task_isRunning(t *testing.T) {
35 - task := newTask(func() {}, time.Second)
36 - assert.True(t, task.isRunning())
48 + for name, tc := range tests {
49 + t.Run(name, func(t *testing.T) {
50 + task := newTask(func() {}, time.Second)
51 + assert.Equal(t, tc.wantBefore, tc.state(task))
52
38 - task.stop()
39 - time.Sleep(time.Millisecond * 500)
40 - assert.False(t, task.isRunning())
53 + task.stop()
54 + task.wait()
55 + assert.Equal(t, tc.wantAfter, tc.state(task))
56 + })
57 + }
58 }
src/go/plugin/go.d/collector/vsphere/taxonomy.yaml new
+430
@@ -0,0 +1,430 @@
1 +taxonomy_version: 1
2 +plugin_name: go.d.plugin
3 +module_name: vsphere
4 +placements:
5 + - id: vsphere
6 + section_id: containers-vms
7 + title: VMware vCenter Server
8 + icon: cgroup
9 + properties: { important: false, grouping: true }
10 + items:
11 + - type: grid
12 + id: vsphere-heads
13 + title: vsphere-heads
14 + items:
15 + - type: context
16 + title: Top Hosts by CPU Usage
17 + contexts: [vsphere.host_cpu_utilization]
18 + chart_library: bars
19 + group_by: [label]
20 + group_by_label: [host]
21 + dimensions_sort: valueDesc
22 + colors: ["#994499", "#22AA99"]
23 + layout: { left: 0, top: 0, width: 3, height: 4 }
24 + - type: context
25 + title: Top Hosts by Memory Usage
26 + contexts: [vsphere.host_mem_utilization]
27 + chart_library: bars
28 + group_by: [label]
29 + group_by_label: [host]
30 + dimensions_sort: valueDesc
31 + colors: ["#DC3912", "#FE3912"]
32 + layout: { left: 3, top: 0, width: 3, height: 4 }
33 + - type: context
34 + title: Top VMs by CPU Usage
35 + contexts: [vsphere.vm_cpu_utilization]
36 + chart_library: bars
37 + group_by: [label]
38 + group_by_label: [vm]
39 + dimensions_sort: valueDesc
40 + colors: ["#994499", "#22AA99"]
41 + layout: { left: 6, top: 0, width: 3, height: 4 }
42 + - type: context
43 + title: Top VMs by Memory Usage
44 + contexts: [vsphere.vm_mem_utilization]
45 + chart_library: bars
46 + group_by: [label]
47 + group_by_label: [vm]
48 + dimensions_sort: valueDesc
49 + colors: ["#990099", "#0099C6"]
50 + layout: { left: 9, top: 0, width: 3, height: 4 }
51 + - type: group
52 + id: inventory
53 + title: Inventory
54 + properties: { grouping: false }
55 + families: false
56 + items:
57 + - vsphere.inventory_objects
58 + - type: group
59 + id: clusters
60 + title: Clusters
61 + properties: { grouping: true }
62 + items:
63 + - type: group
64 + id: cluster-resources
65 + title: Resources
66 + properties: { grouping: false }
67 + families: false
68 + items:
69 + - vsphere.cluster_cpu_capacity
70 + - vsphere.cluster_mem_capacity
71 + - vsphere.cluster_cpu_topology
72 + - type: group
73 + id: cluster-utilization
74 + title: Utilization
75 + properties: { grouping: false }
76 + families: false
77 + items:
78 + - vsphere.cluster_cpu_utilization
79 + - vsphere.cluster_cpu_usage
80 + - vsphere.cluster_mem_utilization
81 + - vsphere.cluster_mem_usage
82 + - vsphere.cluster_usage_cpu
83 + - vsphere.cluster_usage_mem
84 + - type: group
85 + id: cluster-drs
86 + title: Distributed Resource Scheduler
87 + short_name: DRS
88 + properties: { grouping: false }
89 + families: false
90 + items:
91 + - vsphere.cluster_drs_config
92 + - vsphere.cluster_drs_mode
93 + - vsphere.cluster_drs_vmotion_rate
94 + - vsphere.cluster_drs_score
95 + - vsphere.cluster_drs_balance
96 + - vsphere.cluster_services_fairness
97 + - vsphere.cluster_services_effective_cpu
98 + - vsphere.cluster_services_effective_mem
99 + - vsphere.cluster_vmotions
100 + - type: group
101 + id: cluster-ha
102 + title: High Availability
103 + short_name: HA
104 + properties: { grouping: false }
105 + families: false
106 + items:
107 + - vsphere.cluster_ha_config
108 + - vsphere.cluster_ha_host_monitoring
109 + - vsphere.cluster_ha_vm_monitoring
110 + - vsphere.cluster_ha_vm_component_protection
111 + - vsphere.cluster_services_failover
112 + - type: group
113 + id: cluster-vsan
114 + title: vSAN
115 + properties: { grouping: false }
116 + families: false
117 + items:
118 + - vsphere.vsan_cluster_space_usage
119 + - vsphere.vsan_cluster_space_utilization
120 + - vsphere.vsan_cluster_operations
121 + - vsphere.vsan_cluster_throughput
122 + - vsphere.vsan_cluster_latency
123 + - vsphere.vsan_cluster_congestions
124 + - vsphere.vsan_cluster_health_status
125 + - type: group
126 + id: cluster-vm-ops
127 + title: Virtual Machine Operations
128 + short_name: VM Ops
129 + properties: { grouping: false }
130 + families: false
131 + items:
132 + - vsphere.cluster_vm_migrations
133 + - vsphere.cluster_vm_lifecycle
134 + - vsphere.cluster_vm_management
135 + - vsphere.cluster_vm_guest_ops
136 + - vsphere.cluster_vm_cold_migrations
137 + - type: group
138 + id: cluster-inventory
139 + title: Inventory
140 + properties: { grouping: false }
141 + families: false
142 + items:
143 + - vsphere.cluster_hosts
144 + - vsphere.cluster_vm_count
145 + - type: group
146 + id: cluster-status
147 + title: Status
148 + properties: { grouping: false }
149 + families: false
150 + items:
151 + - vsphere.cluster_overall_status
152 + - type: group
153 + id: hosts
154 + title: Hosts
155 + properties: { grouping: true }
156 + items:
157 + - type: group
158 + id: host-cpu
159 + title: CPU
160 + properties: { grouping: false }
161 + families: false
162 + items:
163 + - vsphere.host_cpu_utilization
164 + - type: group
165 + id: host-memory
166 + title: Memory
167 + properties: { grouping: false }
168 + families: false
169 + items:
170 + - vsphere.host_mem_utilization
171 + - vsphere.host_mem_usage
172 + - vsphere.host_mem_swap_io
173 + - type: group
174 + id: host-disk
175 + title: Disk
176 + properties: { grouping: false }
177 + families: false
178 + items:
179 + - vsphere.host_disk_io
180 + - vsphere.host_disk_max_latency
181 + - type: group
182 + id: host-network
183 + title: Network
184 + properties: { grouping: false }
185 + families: false
186 + items:
187 + - vsphere.host_net_traffic
188 + - vsphere.host_net_packets
189 + - vsphere.host_net_drops
190 + - vsphere.host_net_errors
191 + - type: group
192 + id: host-power
193 + title: Power
194 + properties: { grouping: false }
195 + families: false
196 + items:
197 + - vsphere.host_power_usage
198 + - vsphere.host_power_capacity_usage
199 + - vsphere.host_power_capacity_utilization
200 + - vsphere.host_energy_usage
201 + - type: group
202 + id: host-vsan
203 + title: vSAN
204 + properties: { grouping: false }
205 + families: false
206 + items:
207 + - vsphere.vsan_host_operations
208 + - vsphere.vsan_host_throughput
209 + - vsphere.vsan_host_latency
210 + - vsphere.vsan_host_congestions
211 + - vsphere.vsan_host_cache_hit_rate
212 + - type: group
213 + id: host-status
214 + title: Status
215 + properties: { grouping: false }
216 + families: false
217 + items:
218 + - vsphere.host_overall_status
219 + - vsphere.host_power_state
220 + - vsphere.host_connection_state
221 + - vsphere.host_maintenance_status
222 + - type: group
223 + id: host-uptime
224 + title: Uptime
225 + properties: { grouping: false }
226 + families: false
227 + items:
228 + - vsphere.host_system_uptime
229 + - type: group
230 + id: virtual-machines
231 + title: Virtual Machines
232 + short_name: VMs
233 + properties: { grouping: true }
234 + items:
235 + - type: group
236 + id: vm-cpu
237 + title: CPU
238 + properties: { grouping: false }
239 + families: false
240 + items:
241 + - vsphere.vm_cpu_utilization
242 + - type: group
243 + id: vm-memory
244 + title: Memory
245 + properties: { grouping: false }
246 + families: false
247 + items:
248 + - vsphere.vm_mem_utilization
249 + - vsphere.vm_mem_usage
250 + - vsphere.vm_mem_swap_usage
251 + - vsphere.vm_mem_swap_io
252 + - type: group
253 + id: vm-disk
254 + title: Disk
255 + properties: { grouping: false }
256 + families: false
257 + items:
258 + - vsphere.vm_disk_io
259 + - vsphere.vm_disk_max_latency
260 + - type: group
261 + id: vm-storage
262 + title: Storage
263 + properties: { grouping: false }
264 + families: false
265 + items:
266 + - vsphere.vm_storage_usage
267 + - type: group
268 + id: vm-network
269 + title: Network
270 + properties: { grouping: false }
271 + families: false
272 + items:
273 + - vsphere.vm_net_traffic
274 + - vsphere.vm_net_packets
275 + - vsphere.vm_net_drops
276 + - type: group
277 + id: vm-power
278 + title: Power
279 + properties: { grouping: false }
280 + families: false
281 + items:
282 + - vsphere.vm_power_usage
283 + - vsphere.vm_energy_usage
284 + - type: group
285 + id: vm-vsan
286 + title: vSAN
287 + properties: { grouping: false }
288 + families: false
289 + items:
290 + - vsphere.vsan_vm_operations
291 + - vsphere.vsan_vm_throughput
292 + - vsphere.vsan_vm_latency
293 + - type: group
294 + id: vm-snapshots
295 + title: Snapshots
296 + properties: { grouping: false }
297 + families: false
298 + items:
299 + - vsphere.vm_snapshot_count
300 + - vsphere.vm_snapshot_max_age
301 + - vsphere.vm_snapshot_max_chain_depth
302 + - type: group
303 + id: vm-configuration
304 + title: Configuration
305 + short_name: Config
306 + properties: { grouping: false }
307 + families: false
308 + items:
309 + - vsphere.vm_config_cpu
310 + - vsphere.vm_config_memory
311 + - vsphere.vm_config_devices
312 + - type: group
313 + id: vm-status
314 + title: Status
315 + properties: { grouping: false }
316 + families: false
317 + items:
318 + - vsphere.vm_overall_status
319 + - vsphere.vm_power_state
320 + - vsphere.vm_connection_state
321 + - vsphere.vm_consolidation_needed
322 + - type: group
323 + id: vm-tools
324 + title: VMware Tools
325 + short_name: Tools
326 + properties: { grouping: false }
327 + families: false
328 + items:
329 + - vsphere.vm_tools_running_status
330 + - vsphere.vm_tools_version_status
331 + - type: group
332 + id: vm-uptime
333 + title: Uptime
334 + properties: { grouping: false }
335 + families: false
336 + items:
337 + - vsphere.vm_system_uptime
338 + - type: group
339 + id: resource-pools
340 + title: Resource Pools
341 + properties: { grouping: true }
342 + items:
343 + - type: group
344 + id: resource-pool-cpu
345 + title: CPU
346 + properties: { grouping: false }
347 + families: false
348 + items:
349 + - vsphere.resource_pool_cpu_usage
350 + - vsphere.resource_pool_cpu_entitlement
351 + - vsphere.resource_pool_cpu_allocation
352 + - vsphere.resource_pool_cpu_config
353 + - type: group
354 + id: resource-pool-memory
355 + title: Memory
356 + properties: { grouping: false }
357 + families: false
358 + items:
359 + - vsphere.resource_pool_mem_usage
360 + - vsphere.resource_pool_mem_entitlement
361 + - vsphere.resource_pool_mem_allocation
362 + - vsphere.resource_pool_mem_breakdown
363 + - vsphere.resource_pool_mem_config
364 + - type: group
365 + id: resource-pool-status
366 + title: Status
367 + properties: { grouping: false }
368 + families: false
369 + items:
370 + - vsphere.resource_pool_overall_status
371 + - type: group
372 + id: datastores
373 + title: Datastores
374 + properties: { grouping: true }
375 + items:
376 + - type: group
377 + id: datastore-disk-io
378 + title: Disk I/O
379 + properties: { grouping: false }
380 + families: false
381 + items:
382 + - vsphere.datastore_disk_io
383 + - vsphere.datastore_disk_iops
384 + - vsphere.datastore_disk_latency
385 + - type: group
386 + id: datastore-space
387 + title: Space
388 + properties: { grouping: false }
389 + families: false
390 + items:
391 + - vsphere.datastore_space_utilization
392 + - vsphere.datastore_space_usage
393 + - type: group
394 + id: datastore-status
395 + title: Status
396 + properties: { grouping: false }
397 + families: false
398 + items:
399 + - vsphere.datastore_overall_status
400 + - vsphere.datastore_accessibility_status
401 + - vsphere.datastore_maintenance_status
402 + - vsphere.datastore_multiple_host_access
403 + - type: group
404 + id: datastore-clusters
405 + title: Datastore Clusters
406 + short_name: DS Clusters
407 + properties: { grouping: true }
408 + items:
409 + - type: group
410 + id: datastore-cluster-space
411 + title: Space
412 + properties: { grouping: false }
413 + families: false
414 + items:
415 + - vsphere.datastore_cluster_space_utilization
416 + - vsphere.datastore_cluster_space_usage
417 + - type: group
418 + id: datastore-cluster-storage-drs
419 + title: Storage DRS
420 + properties: { grouping: false }
421 + families: false
422 + items:
423 + - vsphere.datastore_cluster_storage_drs_status
424 + - type: group
425 + id: datastore-cluster-status
426 + title: Status
427 + properties: { grouping: false }
428 + families: false
429 + items:
430 + - vsphere.datastore_cluster_overall_status
src/go/plugin/go.d/collector/vsphere/test_helpers_test.go new
+46
@@ -0,0 +1,46 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import (
6 + "sort"
7 + "testing"
8 +
9 + "github.com/netdata/netdata/go/plugins/pkg/metrix"
10 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
11 + "github.com/stretchr/testify/require"
12 +)
13 +
14 +func firstSortedHost(t *testing.T, collr *Collector) *rs.Host {
15 + t.Helper()
16 +
17 + hosts := make([]*rs.Host, 0, len(collr.resources.Hosts))
18 + for _, host := range collr.resources.Hosts {
19 + hosts = append(hosts, host)
20 + }
21 + sort.Slice(hosts, func(i, j int) bool {
22 + return hosts[i].ID < hosts[j].ID
23 + })
24 + require.NotEmpty(t, hosts)
25 + return hosts[0]
26 +}
27 +
28 +func firstSortedVM(t *testing.T, collr *Collector) *rs.VM {
29 + t.Helper()
30 +
31 + vms := sortedVMs(collr.resources.VMs)
32 + require.NotEmpty(t, vms)
33 + return vms[0]
34 +}
35 +
36 +func countMetricSeries(reader metrix.Reader, name string) (count int) {
37 + reader.ForEachByName(name, func(metrix.LabelView, metrix.SampleValue) {
38 + count++
39 + })
40 + return count
41 +}
42 +
43 +//go:fix inline
44 +func boolPtr(value bool) *bool {
45 + return new(value)
46 +}
src/go/plugin/go.d/collector/vsphere/testdata/config.json
+26
@@ -22,6 +22,32 @@
22 "tls_skip_verify": true,
23 "force_http2": true,
24 "discovery_interval": 123.123,
25 + "tag_categories": [
26 + "Environment",
27 + "Business*"
28 + ],
29 + "custom_attributes": [
30 + "Owner",
31 + "Cost Center"
32 + ],
33 + "collect_datastore_clusters": true,
34 + "datastore_cluster_include": [
35 + "/*"
36 + ],
37 + "collect_vsan": true,
38 + "vsan_cluster_include": [
39 + "/DC1/Cluster*",
40 + "vsan_uuid:cluster-uuid"
41 + ],
42 + "vsan_host_include": [
43 + "/DC1/Cluster*/Host*",
44 + "vsan_node_uuid:host-uuid"
45 + ],
46 + "vsan_vm_include": [
47 + "/DC1/Cluster*/Host*/VM*",
48 + "instance_uuid:vm-uuid"
49 + ],
50 + "collect_network_topology": true,
51 "host_include": [
52 "ok"
53 ],
src/go/plugin/go.d/collector/vsphere/testdata/config.yaml
+20
@@ -20,6 +20,26 @@ tls_key: "ok"
20 tls_skip_verify: yes
21 force_http2: yes
22 discovery_interval: 123.123
23 +tag_categories:
24 + - "Environment"
25 + - "Business*"
26 +custom_attributes:
27 + - "Owner"
28 + - "Cost Center"
29 +collect_datastore_clusters: yes
30 +datastore_cluster_include:
31 + - "/*"
32 +collect_vsan: yes
33 +vsan_cluster_include:
34 + - "/DC1/Cluster*"
35 + - "vsan_uuid:cluster-uuid"
36 +vsan_host_include:
37 + - "/DC1/Cluster*/Host*"
38 + - "vsan_node_uuid:host-uuid"
39 +vsan_vm_include:
40 + - "/DC1/Cluster*/Host*/VM*"
41 + - "instance_uuid:vm-uuid"
42 +collect_network_topology: yes
43 host_include:
44 - "ok"
45 vm_include:
src/go/plugin/go.d/collector/vsphere/vsan.go new
+226
@@ -0,0 +1,226 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/pkg/metrix"
7 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
8 + scrapepkg "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/scrape"
9 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/oldmetrix"
10 +)
11 +
12 +const (
13 + vsanClusterSpaceUsageTotalMetric = "vsan_cluster_space_usage_total"
14 + vsanClusterSpaceUsageFreeMetric = "vsan_cluster_space_usage_free"
15 + vsanClusterSpaceUsageUsedMetric = "vsan_cluster_space_usage_used"
16 + vsanClusterSpaceUtilizationUsedMetric = "vsan_cluster_space_utilization_used"
17 + vsanClusterHealthStatusGreenMetric = "vsan_cluster_health_status_green"
18 + vsanClusterHealthStatusYellowMetric = "vsan_cluster_health_status_yellow"
19 + vsanClusterHealthStatusRedMetric = "vsan_cluster_health_status_red"
20 + vsanClusterHealthStatusUnknownMetric = "vsan_cluster_health_status_unknown"
21 + vsanClusterOperationsReadMetric = "vsan_cluster_operations_read"
22 + vsanClusterOperationsWriteMetric = "vsan_cluster_operations_write"
23 + vsanClusterThroughputReadMetric = "vsan_cluster_throughput_read"
24 + vsanClusterThroughputWriteMetric = "vsan_cluster_throughput_write"
25 + vsanClusterLatencyReadMetric = "vsan_cluster_latency_read"
26 + vsanClusterLatencyWriteMetric = "vsan_cluster_latency_write"
27 + vsanClusterCongestionsMetric = "vsan_cluster_congestions"
28 + vsanHostOperationsReadMetric = "vsan_host_operations_read"
29 + vsanHostOperationsWriteMetric = "vsan_host_operations_write"
30 + vsanHostThroughputReadMetric = "vsan_host_throughput_read"
31 + vsanHostThroughputWriteMetric = "vsan_host_throughput_write"
32 + vsanHostLatencyReadMetric = "vsan_host_latency_read"
33 + vsanHostLatencyWriteMetric = "vsan_host_latency_write"
34 + vsanHostCongestionsMetric = "vsan_host_congestions"
35 + vsanHostCacheHitRateMetric = "vsan_host_cache_hit_rate"
36 + vsanVMOperationsReadMetric = "vsan_vm_operations_read"
37 + vsanVMOperationsWriteMetric = "vsan_vm_operations_write"
38 + vsanVMThroughputReadMetric = "vsan_vm_throughput_read"
39 + vsanVMThroughputWriteMetric = "vsan_vm_throughput_write"
40 + vsanVMLatencyReadMetric = "vsan_vm_latency_read"
41 + vsanVMLatencyWriteMetric = "vsan_vm_latency_write"
42 + vsanUUIDLabel = "vsan_uuid"
43 + vsanNodeUUIDLabel = "vsan_node_uuid"
44 + vmInstanceUUIDLabel = "vm_instance_uuid"
45 +)
46 +
47 +func (c *Collector) writeVSANMetrics() {
48 + if !c.CollectVSAN || c.resources == nil || c.vsanMetrics == nil {
49 + return
50 + }
51 + c.writeVSANClusterSpaceMetrics()
52 + c.writeVSANClusterHealthMetrics()
53 + c.writeVSANClusterPerformanceMetrics()
54 + c.writeVSANHostPerformanceMetrics()
55 + c.writeVSANVMPerformanceMetrics()
56 +}
57 +
58 +func (c *Collector) vsanResources() (rs.Clusters, rs.Hosts, rs.VMs) {
59 + clusters := make(rs.Clusters)
60 + hosts := make(rs.Hosts)
61 + vms := make(rs.VMs)
62 +
63 + clusterMatcher := c.vsanClusterMatcher
64 + hostMatcher := c.vsanHostMatcher
65 + vmMatcher := c.vsanVMMatcher
66 +
67 + selectedClusters := make(map[string]bool)
68 + for _, cluster := range sortedClusters(c.resources.Clusters) {
69 + if !cluster.VSANEnabled || (clusterMatcher != nil && !clusterMatcher.Match(cluster)) {
70 + continue
71 + }
72 + clusters[cluster.ID] = cluster
73 + selectedClusters[cluster.ID] = true
74 + }
75 +
76 + for _, host := range sortedHosts(c.resources.Hosts) {
77 + if !selectedClusters[host.Hier.Cluster.ID] || host.VSANNodeUUID == "" || (hostMatcher != nil && !hostMatcher.Match(host)) {
78 + continue
79 + }
80 + hosts[host.ID] = host
81 + }
82 +
83 + for _, vm := range sortedVMs(c.resources.VMs) {
84 + if !selectedClusters[vm.Hier.Cluster.ID] || vm.InstanceUUID == "" || (vmMatcher != nil && !vmMatcher.Match(vm)) {
85 + continue
86 + }
87 + vms[vm.ID] = vm
88 + }
89 +
90 + return clusters, hosts, vms
91 +}
92 +
93 +func (c *Collector) writeVSANClusterSpaceMetrics() {
94 + for _, id := range sortedMapKeys(c.vsanMetrics.Space) {
95 + cluster := c.resources.Clusters.Get(id)
96 + if cluster == nil {
97 + continue
98 + }
99 + space := c.vsanMetrics.Space[id]
100 + used := max(space.Total-space.Free, 0)
101 + labels := c.labelSet(c.vsanClusterLabels(cluster))
102 + c.observeGaugeFloat(vsanClusterSpaceUsageTotalMetric, float64(space.Total), labels)
103 + c.observeGaugeFloat(vsanClusterSpaceUsageFreeMetric, float64(space.Free), labels)
104 + c.observeGaugeFloat(vsanClusterSpaceUsageUsedMetric, float64(used), labels)
105 + if space.Total > 0 {
106 + c.observeGaugeFloat(vsanClusterSpaceUtilizationUsedMetric, float64(used)/float64(space.Total)*scaledPercent, labels)
107 + } else {
108 + c.observeGaugeFloat(vsanClusterSpaceUtilizationUsedMetric, 0, labels)
109 + }
110 + }
111 +}
112 +
113 +func (c *Collector) writeVSANClusterHealthMetrics() {
114 + for _, id := range sortedMapKeys(c.vsanMetrics.Health) {
115 + cluster := c.resources.Clusters.Get(id)
116 + if cluster == nil {
117 + continue
118 + }
119 + health := c.vsanMetrics.Health[id]
120 + labels := c.labelSet(c.vsanClusterLabels(cluster))
121 + c.observeGaugeFloat(vsanClusterHealthStatusGreenMetric, float64(oldmetrix.Bool(health == "green")), labels)
122 + c.observeGaugeFloat(vsanClusterHealthStatusYellowMetric, float64(oldmetrix.Bool(health == "yellow")), labels)
123 + c.observeGaugeFloat(vsanClusterHealthStatusRedMetric, float64(oldmetrix.Bool(health == "red")), labels)
124 + c.observeGaugeFloat(vsanClusterHealthStatusUnknownMetric, float64(oldmetrix.Bool(health != "green" && health != "yellow" && health != "red")), labels)
125 + }
126 +}
127 +
128 +func (c *Collector) writeVSANClusterPerformanceMetrics() {
129 + for _, id := range sortedMapKeys(c.vsanMetrics.Clusters) {
130 + cluster := c.resources.Clusters.Get(id)
131 + if cluster == nil {
132 + continue
133 + }
134 + labels := c.labelSet(c.vsanClusterLabels(cluster))
135 + writeVSANPerformanceValues(c, labels, c.vsanMetrics.Clusters[id], vsanClusterPerfMetricByName)
136 + }
137 +}
138 +
139 +func (c *Collector) writeVSANHostPerformanceMetrics() {
140 + for _, id := range sortedMapKeys(c.vsanMetrics.Hosts) {
141 + host := c.resources.Hosts.Get(id)
142 + if host == nil {
143 + continue
144 + }
145 + labels := c.labelSet(c.vsanHostLabels(host))
146 + writeVSANPerformanceValues(c, labels, c.vsanMetrics.Hosts[id], vsanHostPerfMetricByName)
147 + }
148 +}
149 +
150 +func (c *Collector) writeVSANVMPerformanceMetrics() {
151 + for _, id := range sortedMapKeys(c.vsanMetrics.VMs) {
152 + vm := c.resources.VMs.Get(id)
153 + if vm == nil {
154 + continue
155 + }
156 + labels := c.labelSet(c.vsanVMLabels(vm))
157 + writeVSANPerformanceValues(c, labels, c.vsanMetrics.VMs[id], vsanVMPerfMetricByName)
158 + }
159 +}
160 +
161 +func writeVSANPerformanceValues(c *Collector, labels metrix.LabelSet, values scrapepkg.VSANEntityMetrics, metricByName map[string]string) {
162 + for _, name := range sortedMapKeys(values) {
163 + metricName := metricByName[name]
164 + if metricName == "" {
165 + continue
166 + }
167 + c.observeGaugeFloat(metricName, values[name], labels)
168 + }
169 +}
170 +
171 +var vsanClusterPerfMetricByName = map[string]string{
172 + "read_operations": vsanClusterOperationsReadMetric,
173 + "write_operations": vsanClusterOperationsWriteMetric,
174 + "read_throughput": vsanClusterThroughputReadMetric,
175 + "write_throughput": vsanClusterThroughputWriteMetric,
176 + "read_latency": vsanClusterLatencyReadMetric,
177 + "write_latency": vsanClusterLatencyWriteMetric,
178 + "congestions": vsanClusterCongestionsMetric,
179 +}
180 +
181 +var vsanHostPerfMetricByName = map[string]string{
182 + "read_operations": vsanHostOperationsReadMetric,
183 + "write_operations": vsanHostOperationsWriteMetric,
184 + "read_throughput": vsanHostThroughputReadMetric,
185 + "write_throughput": vsanHostThroughputWriteMetric,
186 + "read_latency": vsanHostLatencyReadMetric,
187 + "write_latency": vsanHostLatencyWriteMetric,
188 + "congestions": vsanHostCongestionsMetric,
189 + "cache_hit_rate": vsanHostCacheHitRateMetric,
190 +}
191 +
192 +var vsanVMPerfMetricByName = map[string]string{
193 + "read_operations": vsanVMOperationsReadMetric,
194 + "write_operations": vsanVMOperationsWriteMetric,
195 + "read_throughput": vsanVMThroughputReadMetric,
196 + "write_throughput": vsanVMThroughputWriteMetric,
197 + "read_latency": vsanVMLatencyReadMetric,
198 + "write_latency": vsanVMLatencyWriteMetric,
199 +}
200 +
201 +func (c *Collector) vsanClusterLabels(cluster *rs.Cluster) []metrix.Label {
202 + return c.v2MetricLabels(cluster.ID, []metrix.Label{
203 + {Key: "datacenter", Value: cluster.Hier.DC.Name},
204 + {Key: "cluster", Value: cluster.Name},
205 + {Key: vsanUUIDLabel, Value: cluster.VSANUUID},
206 + }, cluster.Labels)
207 +}
208 +
209 +func (c *Collector) vsanHostLabels(host *rs.Host) []metrix.Label {
210 + return c.v2MetricLabels(host.ID, []metrix.Label{
211 + {Key: "datacenter", Value: host.Hier.DC.Name},
212 + {Key: "cluster", Value: getHostClusterName(host)},
213 + {Key: "host", Value: host.Name},
214 + {Key: vsanNodeUUIDLabel, Value: host.VSANNodeUUID},
215 + }, host.Labels)
216 +}
217 +
218 +func (c *Collector) vsanVMLabels(vm *rs.VM) []metrix.Label {
219 + return c.v2MetricLabels(vm.ID, []metrix.Label{
220 + {Key: "datacenter", Value: vm.Hier.DC.Name},
221 + {Key: "cluster", Value: getVMClusterName(vm)},
222 + {Key: "host", Value: vm.Hier.Host.Name},
223 + {Key: "vm", Value: vm.Name},
224 + {Key: vmInstanceUUIDLabel, Value: vm.InstanceUUID},
225 + }, vm.Labels)
226 +}
src/go/plugin/go.d/collector/vsphere/vsan_test.go new
+323
@@ -0,0 +1,323 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package vsphere
4 +
5 +import (
6 + "testing"
7 +
8 + "github.com/stretchr/testify/require"
9 + "github.com/vmware/govmomi/performance"
10 +
11 + "github.com/netdata/netdata/go/plugins/pkg/metrix"
12 + "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/match"
13 + rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
14 + scrapepkg "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/scrape"
15 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
16 +)
17 +
18 +func TestCollector_VSANMetricsDefaultOff(t *testing.T) {
19 + collr := newVSANTestCollector(false)
20 + cycle := mustCycleController(t, collr.MetricStore())
21 + cycle.BeginCycle()
22 + collr.writeVSANMetrics()
23 + require.NoError(t, cycle.CommitCycleSuccess())
24 +
25 + reader := collr.MetricStore().Read(metrix.ReadRaw())
26 + require.Zero(t, countMetricSeries(reader, vsanClusterSpaceUsageTotalMetric))
27 + require.Zero(t, countMetricSeries(reader, vsanHostOperationsReadMetric))
28 + require.Zero(t, countMetricSeries(reader, vsanVMOperationsReadMetric))
29 +}
30 +
31 +func TestCollector_VSANMetricsOptInEmitsCharts(t *testing.T) {
32 + collr := newVSANTestCollector(true)
33 + cycle := mustCycleController(t, collr.MetricStore())
34 + cycle.BeginCycle()
35 + collr.writeVSANMetrics()
36 + require.NoError(t, cycle.CommitCycleSuccess())
37 +
38 + cluster := collr.resources.Clusters.Get("domain-c1")
39 + host := collr.resources.Hosts.Get("host-1")
40 + vm := collr.resources.VMs.Get("vm-1")
41 + reader := collr.MetricStore().Read(metrix.ReadRaw())
42 +
43 + clusterLabels := labelsFromMetrix(collr.vsanClusterLabels(cluster))
44 + requireMetricValue(t, reader, vsanClusterSpaceUsageTotalMetric, clusterLabels, 1000)
45 + requireMetricValue(t, reader, vsanClusterSpaceUsageFreeMetric, clusterLabels, 400)
46 + requireMetricValue(t, reader, vsanClusterSpaceUsageUsedMetric, clusterLabels, 600)
47 + requireMetricValue(t, reader, vsanClusterSpaceUtilizationUsedMetric, clusterLabels, 6000)
48 + requireMetricValue(t, reader, vsanClusterHealthStatusGreenMetric, clusterLabels, 1)
49 + requireMetricValue(t, reader, vsanClusterOperationsReadMetric, clusterLabels, 10)
50 + requireMetricValue(t, reader, vsanClusterThroughputReadMetric, clusterLabels, 11)
51 + requireMetricValue(t, reader, vsanClusterLatencyReadMetric, clusterLabels, 12)
52 + requireMetricValue(t, reader, vsanClusterCongestionsMetric, clusterLabels, 13)
53 + requireMetricValue(t, reader, vsanClusterOperationsWriteMetric, clusterLabels, 14)
54 + requireMetricValue(t, reader, vsanClusterThroughputWriteMetric, clusterLabels, 15)
55 + requireMetricValue(t, reader, vsanClusterLatencyWriteMetric, clusterLabels, 16)
56 +
57 + hostLabels := labelsFromMetrix(collr.vsanHostLabels(host))
58 + requireMetricValue(t, reader, vsanHostOperationsReadMetric, hostLabels, 20)
59 + requireMetricValue(t, reader, vsanHostThroughputReadMetric, hostLabels, 21)
60 + requireMetricValue(t, reader, vsanHostLatencyReadMetric, hostLabels, 22)
61 + requireMetricValue(t, reader, vsanHostCongestionsMetric, hostLabels, 23)
62 + requireMetricValue(t, reader, vsanHostCacheHitRateMetric, hostLabels, 95)
63 + requireMetricValue(t, reader, vsanHostOperationsWriteMetric, hostLabels, 24)
64 + requireMetricValue(t, reader, vsanHostThroughputWriteMetric, hostLabels, 25)
65 + requireMetricValue(t, reader, vsanHostLatencyWriteMetric, hostLabels, 26)
66 +
67 + vmLabels := labelsFromMetrix(collr.vsanVMLabels(vm))
68 + requireMetricValue(t, reader, vsanVMOperationsReadMetric, vmLabels, 30)
69 + requireMetricValue(t, reader, vsanVMThroughputReadMetric, vmLabels, 31)
70 + requireMetricValue(t, reader, vsanVMLatencyReadMetric, vmLabels, 32)
71 + requireMetricValue(t, reader, vsanVMOperationsWriteMetric, vmLabels, 33)
72 + requireMetricValue(t, reader, vsanVMThroughputWriteMetric, vmLabels, 34)
73 + requireMetricValue(t, reader, vsanVMLatencyWriteMetric, vmLabels, 35)
74 +
75 + createdCharts, createdDims := v2CreatedChartsAndDims(buildV2PlanForTest(t, collr))
76 + chartID := findChartIDByLabelsAndContext(t, createdCharts, "vsphere.vsan_cluster_space_usage", map[string]string{"id": cluster.ID})
77 + require.Contains(t, createdDims[chartID], "used")
78 + hostChartID := findChartIDByLabelsAndContext(t, createdCharts, "vsphere.vsan_host_operations", map[string]string{"id": host.ID})
79 + require.Contains(t, createdDims[hostChartID], "read")
80 + vmChartID := findChartIDByLabelsAndContext(t, createdCharts, "vsphere.vsan_vm_operations", map[string]string{"id": vm.ID})
81 + require.Contains(t, createdDims[vmChartID], "read")
82 + collecttest.AssertChartCoverage(t, collr, collecttest.ChartCoverageExpectation{})
83 + requireChartSelectorsMatchSeries(t, collr, "vsphere.vsan_")
84 +}
85 +
86 +func TestCollector_VSANSpaceUsageEdgeCases(t *testing.T) {
87 + tests := map[string]struct {
88 + space scrapepkg.VSANSpaceUsage
89 + want map[string]int64
90 + }{
91 + "zero total": {
92 + space: scrapepkg.VSANSpaceUsage{Total: 0, Free: 0},
93 + want: map[string]int64{
94 + vsanClusterSpaceUsageUsedMetric: 0,
95 + vsanClusterSpaceUtilizationUsedMetric: 0,
96 + },
97 + },
98 + "free greater than total": {
99 + space: scrapepkg.VSANSpaceUsage{Total: 100, Free: 200},
100 + want: map[string]int64{
101 + vsanClusterSpaceUsageUsedMetric: 0,
102 + vsanClusterSpaceUtilizationUsedMetric: 0,
103 + },
104 + },
105 + }
106 +
107 + for name, tc := range tests {
108 + t.Run(name, func(t *testing.T) {
109 + collr := newVSANTestCollector(true)
110 + collr.vsanMetrics.Space["domain-c1"] = tc.space
111 + cycle := mustCycleController(t, collr.MetricStore())
112 + cycle.BeginCycle()
113 + collr.writeVSANMetrics()
114 + require.NoError(t, cycle.CommitCycleSuccess())
115 +
116 + labels := labelsFromMetrix(collr.vsanClusterLabels(collr.resources.Clusters.Get("domain-c1")))
117 + reader := collr.MetricStore().Read(metrix.ReadRaw())
118 + for metric, want := range tc.want {
119 + requireMetricValue(t, reader, metric, labels, want)
120 + }
121 + })
122 + }
123 +}
124 +
125 +func TestCollector_CollectVSANUsesSelectors(t *testing.T) {
126 + collr := New()
127 + collr.URL = "https://127.0.0.1"
128 + collr.Username = "user"
129 + collr.Password = "pass"
130 + collr.CollectVSAN = true
131 + collr.VSANClustersInclude = match.VSANClusterIncludes{"vsan_uuid:cluster-uuid-2"}
132 + collr.VSANHostsInclude = match.VSANHostIncludes{"vsan_node_uuid:host-uuid-2"}
133 + collr.VSANVMsInclude = match.VSANVMIncludes{"instance_uuid:vm-uuid-2"}
134 + collr.resources = newVSANFilterTestResources()
135 + scraper := &capturingVSANScraper{}
136 + collr.scraper = scraper
137 +
138 + require.NoError(t, collr.validateConfig())
139 + collr.collectVSAN()
140 +
141 + require.NotNil(t, collr.vsanMetrics)
142 + require.Contains(t, scraper.clusters, "domain-c2")
143 + require.NotContains(t, scraper.clusters, "domain-c1")
144 + require.Contains(t, scraper.hosts, "host-2")
145 + require.NotContains(t, scraper.hosts, "host-1")
146 + require.Contains(t, scraper.vms, "vm-2")
147 + require.NotContains(t, scraper.vms, "vm-1")
148 + require.Len(t, scraper.clusters, 1)
149 + require.Len(t, scraper.hosts, 1)
150 + require.Len(t, scraper.vms, 1)
151 +}
152 +
153 +func newVSANTestCollector(enabled bool) *Collector {
154 + collr := New()
155 + collr.CollectVSAN = enabled
156 + collr.resources = &rs.Resources{
157 + Clusters: rs.Clusters{
158 + "domain-c1": &rs.Cluster{
159 + ID: "domain-c1",
160 + Name: "Cluster1",
161 + Hier: rs.ClusterHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}},
162 + VSANEnabled: true,
163 + VSANUUID: "cluster-uuid",
164 + },
165 + },
166 + Hosts: rs.Hosts{
167 + "host-1": &rs.Host{
168 + ID: "host-1",
169 + Name: "Host1",
170 + Hier: rs.HostHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c1", Name: "Cluster1"}},
171 + VSANNodeUUID: "host-uuid",
172 + },
173 + },
174 + VMs: rs.VMs{
175 + "vm-1": &rs.VM{
176 + ID: "vm-1",
177 + Name: "VM1",
178 + Hier: rs.VMHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c1", Name: "Cluster1"}, Host: rs.HierarchyValue{ID: "host-1", Name: "Host1"}},
179 + InstanceUUID: "vm-uuid",
180 + },
181 + },
182 + }
183 + collr.vsanMetrics = &scrapepkg.VSANMetrics{
184 + Clusters: map[string]scrapepkg.VSANEntityMetrics{
185 + "domain-c1": {
186 + "read_operations": 10,
187 + "read_throughput": 11,
188 + "read_latency": 12,
189 + "congestions": 13,
190 + "write_operations": 14,
191 + "write_throughput": 15,
192 + "write_latency": 16,
193 + },
194 + },
195 + Hosts: map[string]scrapepkg.VSANEntityMetrics{
196 + "host-1": {
197 + "read_operations": 20,
198 + "read_throughput": 21,
199 + "read_latency": 22,
200 + "congestions": 23,
201 + "write_operations": 24,
202 + "write_throughput": 25,
203 + "write_latency": 26,
204 + "cache_hit_rate": 95,
205 + },
206 + },
207 + VMs: map[string]scrapepkg.VSANEntityMetrics{
208 + "vm-1": {
209 + "read_operations": 30,
210 + "read_throughput": 31,
211 + "read_latency": 32,
212 + "write_operations": 33,
213 + "write_throughput": 34,
214 + "write_latency": 35,
215 + },
216 + },
217 + Space: map[string]scrapepkg.VSANSpaceUsage{
218 + "domain-c1": {Total: 1000, Free: 400},
219 + },
220 + Health: map[string]string{
221 + "domain-c1": "green",
222 + },
223 + }
224 + return collr
225 +}
226 +
227 +func newVSANFilterTestResources() *rs.Resources {
228 + return &rs.Resources{
229 + Clusters: rs.Clusters{
230 + "domain-c1": &rs.Cluster{
231 + ID: "domain-c1",
232 + Name: "Cluster1",
233 + Hier: rs.ClusterHierarchy{DC: rs.HierarchyValue{Name: "DC1"}},
234 + VSANEnabled: true,
235 + VSANUUID: "cluster-uuid-1",
236 + },
237 + "domain-c2": &rs.Cluster{
238 + ID: "domain-c2",
239 + Name: "Cluster2",
240 + Hier: rs.ClusterHierarchy{DC: rs.HierarchyValue{Name: "DC1"}},
241 + VSANEnabled: true,
242 + VSANUUID: "cluster-uuid-2",
243 + },
244 + },
245 + Hosts: rs.Hosts{
246 + "host-1": &rs.Host{
247 + ID: "host-1",
248 + Name: "Host1",
249 + Hier: rs.HostHierarchy{DC: rs.HierarchyValue{Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c1", Name: "Cluster1"}},
250 + VSANNodeUUID: "host-uuid-1",
251 + },
252 + "host-2": &rs.Host{
253 + ID: "host-2",
254 + Name: "Host2",
255 + Hier: rs.HostHierarchy{DC: rs.HierarchyValue{Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c2", Name: "Cluster2"}},
256 + VSANNodeUUID: "host-uuid-2",
257 + },
258 + "host-3": &rs.Host{
259 + ID: "host-3",
260 + Name: "Host3",
261 + Hier: rs.HostHierarchy{DC: rs.HierarchyValue{Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c2", Name: "Cluster2"}},
262 + VSANNodeUUID: "host-uuid-3",
263 + },
264 + },
265 + VMs: rs.VMs{
266 + "vm-1": &rs.VM{
267 + ID: "vm-1",
268 + Name: "VM1",
269 + Hier: rs.VMHierarchy{DC: rs.HierarchyValue{Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c1", Name: "Cluster1"}, Host: rs.HierarchyValue{ID: "host-1", Name: "Host1"}},
270 + InstanceUUID: "vm-uuid-1",
271 + },
272 + "vm-2": &rs.VM{
273 + ID: "vm-2",
274 + Name: "VM2",
275 + Hier: rs.VMHierarchy{DC: rs.HierarchyValue{Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c2", Name: "Cluster2"}, Host: rs.HierarchyValue{ID: "host-2", Name: "Host2"}},
276 + InstanceUUID: "vm-uuid-2",
277 + },
278 + "vm-3": &rs.VM{
279 + ID: "vm-3",
280 + Name: "VM3",
281 + Hier: rs.VMHierarchy{DC: rs.HierarchyValue{Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c2", Name: "Cluster2"}, Host: rs.HierarchyValue{ID: "host-3", Name: "Host3"}},
282 + InstanceUUID: "vm-uuid-3",
283 + },
284 + },
285 + }
286 +}
287 +
288 +type capturingVSANScraper struct {
289 + clusters rs.Clusters
290 + hosts rs.Hosts
291 + vms rs.VMs
292 +}
293 +
294 +func (s *capturingVSANScraper) ScrapeHosts(rs.Hosts) []performance.EntityMetric {
295 + return nil
296 +}
297 +
298 +func (s *capturingVSANScraper) ScrapeVMs(rs.VMs) []performance.EntityMetric {
299 + return nil
300 +}
301 +
302 +func (s *capturingVSANScraper) ScrapeDatastores(rs.Datastores) []performance.EntityMetric {
303 + return nil
304 +}
305 +
306 +func (s *capturingVSANScraper) ScrapeClusters(rs.Clusters) []performance.EntityMetric {
307 + return nil
308 +}
309 +
310 +func (s *capturingVSANScraper) ScrapeVSAN(clusters rs.Clusters, hosts rs.Hosts, vms rs.VMs) *scrapepkg.VSANMetrics {
311 + s.clusters = clusters
312 + s.hosts = hosts
313 + s.vms = vms
314 + return &scrapepkg.VSANMetrics{}
315 +}
316 +
317 +func labelsFromMetrix(labels []metrix.Label) metrix.Labels {
318 + out := make(metrix.Labels, len(labels))
319 + for _, label := range labels {
320 + out[label.Key] = label.Value
321 + }
322 + return out
323 +}
src/go/plugin/go.d/config/go.d/vsphere.conf
+148 -10
@@ -1,13 +1,151 @@
1 -## All available configuration options, their descriptions and default values:
1 +## VMware vCenter Server collector.
2 +##
3 +## Full option reference:
4 ## https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/vsphere#readme
5 +##
6 +## One job connects to one vCenter endpoint and discovers the included ESXi
7 +## hosts, VMs, datastores, clusters, and resource pools reachable through it.
8 +## By default, inventory and optional resource include selectors are "/*",
9 +## which means "include all" for their respective surfaces.
10 +## Hosts and VMs are discovered regardless of power state when vSphere returns
11 +## them. Non-powered-on hosts/VMs do not get real-time performance counters.
12 +##
13 +## Selector formats:
14 +## host_include : /Datacenter/Cluster/Host
15 +## vm_include : /Datacenter/Cluster/Host/VM
16 +## datastore_include : /Datacenter/Datastore
17 +## cluster_include : /Datacenter/Cluster
18 +## datastore_cluster_include : /Datacenter/DatastoreCluster
19 +##
20 +## Selector values use Netdata simple patterns. Resource pools follow their
21 +## owning cluster selector.
22 +##
23 +## Credentials can be plain YAML values, but using Netdata secret resolvers is
24 +## safer:
25 +## "${env:VSPHERE_PASSWORD}"
26 +## "${file:/run/secrets/vsphere_password}"
27 +## "${cmd:/absolute/path/to/command args}"
28 +## "${store:<kind>:<name>:<operand>}"
29 +##
30 +## Set vnode only if you want all metrics from this job attached to a Netdata
31 +## Virtual Node. Leaving vnode unset preserves the normal local-node behavior.
32 +##
33 +## tag_categories and custom_attributes are opt-in label enrichment.
34 +## They are disabled by default because tags and custom attributes can expose
35 +## internal naming, ownership, addressing, or business metadata.
36 +##
37 +## collect_datastore_clusters and collect_vsan are opt-in metric collection
38 +## groups.
39 +## Datastore clusters add aggregate capacity/status for
40 +## StoragePod objects. Host and VM power/energy plus host power-capacity
41 +## performance counters are collected when vSphere exposes them.
42 +## collect_vsan adds vSAN cluster capacity, vSAN cluster health, and vSAN
43 +## cluster/host/VM performance metrics through vSAN APIs. It queries concrete
44 +## discovered vSAN clusters, hosts, and VMs. Use vsan_*_include selectors to
45 +## choose both emitted series and vSAN performance query scope. It requires vSAN
46 +## and the vSAN Performance Service/API to be available.
47 +## collect_network_topology is also opt-in, but it is not a metric group. It
48 +## discovers vSphere Network and Distributed Virtual Port Group objects for the
49 +## cached vSphere Topology function. It defaults to no so existing users do not
50 +## get extra vCenter discovery calls unless they ask for network topology.
51 +##
52 +## discovery_interval controls inventory refreshes. Keep it reasonably high on
53 +## large vCenters to avoid unnecessary vCenter load.
54
55 #jobs:
5 -# - name : vcenter1
6 -# url : https://203.0.113.0
7 -# username : admin@vsphere.local
8 -# password : password
9 -#
10 -# - name : vcenter2
11 -# url : https://203.0.113.10
12 -# username : admin@vsphere.local
13 -# password : password
56 +# - name: vcenter1
57 +# url: https://vcenter.local
58 +# username: admin@vsphere.local
59 +# password: "${file:/run/secrets/vsphere_password}"
60 +#
61 +# # Optional. Defaults to 20 seconds.
62 +# #update_every: 20
63 +#
64 +# # Optional. Defaults to 60 seconds. Set 0 to disable autodetection retry.
65 +# #autodetection_retry: 60
66 +#
67 +# # Optional. Defaults to 5 minutes.
68 +# #discovery_interval: 300
69 +#
70 +# # Optional. Associate this job with a Netdata Virtual Node.
71 +# #vnode: vcenter1
72 +#
73 +# # Optional vSphere tag category allowlist. Defaults to empty. Each list
74 +# # item is one glob pattern matching category names, so names with spaces
75 +# # are supported. Use "*" only when all tag categories are intentional.
76 +# # Label keys become vsphere_tag_<sanitized_category>.
77 +# # Multiple tags in the same category are sorted and joined with "|".
78 +# #tag_categories:
79 +# # - "Environment"
80 +# # - "Business Unit"
81 +#
82 +# # Optional vSphere custom attribute allowlist. Defaults to empty. Each list
83 +# # item is one glob pattern matching attribute names, so names with spaces
84 +# # are supported. Custom attribute values are sent verbatim as labels. Use
85 +# # "*" only when all custom attributes are intentional and none of the
86 +# # matched values contain secrets.
87 +# # Label keys become vsphere_custom_attribute_<sanitized_name>.
88 +# #custom_attributes:
89 +# # - "Owner"
90 +# # - "Cost Center"
91 +#
92 +# # Optional. Defaults to false. Adds aggregate datastore-cluster
93 +# # (StoragePod) capacity and Storage DRS status.
94 +# #collect_datastore_clusters: no
95 +#
96 +# # Optional datastore-cluster selector. Defaults to all datastore clusters
97 +# # when collect_datastore_clusters is enabled. Patterns match
98 +# # /Datacenter/DatastoreCluster, datastore-cluster name, or vSphere ID.
99 +# # Matching datastore clusters are included in metrics and topology data.
100 +# #datastore_cluster_include:
101 +# # - "/*"
102 +#
103 +# # Optional. Defaults to false. Adds vSAN cluster capacity, vSAN cluster
104 +# # health, and vSAN cluster/host/VM performance metrics. It queries only
105 +# # discovered resources that pass the vSAN selectors below.
106 +# # Requires vSAN and the vSAN Performance Service/API.
107 +# #collect_vsan: no
108 +#
109 +# # Optional. Defaults to all discovered vSAN-enabled clusters. Applies only
110 +# # when collect_vsan is enabled. Patterns match /Datacenter/Cluster,
111 +# # cluster name, vSphere managed object ID, or vsan_uuid:<uuid>.
112 +# #vsan_cluster_include:
113 +# # - "/*"
114 +#
115 +# # Optional. Defaults to all discovered hosts in selected vSAN clusters.
116 +# # Applies only when collect_vsan is enabled. Patterns match
117 +# # /Datacenter/Cluster/Host, host name, vSphere managed object ID, or
118 +# # vsan_node_uuid:<uuid>.
119 +# #vsan_host_include:
120 +# # - "/*"
121 +#
122 +# # Optional. Defaults to all discovered VMs in selected vSAN clusters.
123 +# # Applies only when collect_vsan is enabled. Patterns match
124 +# # /Datacenter/Cluster/Host/VM, VM name, vSphere managed object ID, or
125 +# # instance_uuid:<uuid>.
126 +# #vsan_vm_include:
127 +# # - "/*"
128 +#
129 +# # Optional. Defaults to false. Discovers vSphere Network and Distributed
130 +# # Virtual Port Group objects for the cached vSphere Topology function. It
131 +# # does not create charts or metrics.
132 +# #collect_network_topology: no
133 +#
134 +# # Optional selectors. Defaults include everything.
135 +# #host_include:
136 +# # - "/*"
137 +# #vm_include:
138 +# # - "/*"
139 +# #datastore_include:
140 +# # - "/*"
141 +# #cluster_include:
142 +# # - "/*"
143 +#
144 +# # Optional HTTP/TLS settings.
145 +# #timeout: 20
146 +# #tls_skip_verify: false
147 +#
148 +# - name: vcenter2
149 +# url: https://vcenter2.local
150 +# username: admin@vsphere.local
151 +# password: "${env:VSPHERE2_PASSWORD}"
src/health/health.d/vsphere.conf
+31 -3
@@ -33,12 +33,40 @@ component: Memory
33 info: Memory utilization VM ${label:vm} host ${label:host} cluster ${label:cluster} datacenter ${label:datacenter}
34 to: silent
35
36 + template: vsphere_vm_snapshot_chain_depth
37 + on: vsphere.vm_snapshot_max_chain_depth
38 + class: Errors
39 + type: Virtual Machine
40 +component: Storage
41 + calc: $depth
42 + units: snapshots
43 + every: 20s
44 + warn: $this > 3
45 + delay: down 15m multiplier 1.5 max 1h
46 + summary: vSphere snapshot chain depth for VM ${label:vm}
47 + info: Snapshot max chain depth for VM ${label:vm} host ${label:host} cluster ${label:cluster} datacenter ${label:datacenter}; zero means no snapshots
48 + to: sysadmin
49 +
50 + template: vsphere_vm_snapshot_age
51 + on: vsphere.vm_snapshot_max_age
52 + class: Errors
53 + type: Virtual Machine
54 +component: Storage
55 + calc: $age
56 + units: seconds
57 + every: 20s
58 + crit: $this > 86400
59 + delay: down 15m multiplier 1.5 max 1h
60 + summary: vSphere oldest snapshot age for VM ${label:vm}
61 + info: Oldest snapshot age for VM ${label:vm} host ${label:host} cluster ${label:cluster} datacenter ${label:datacenter}; zero means no snapshots
62 + to: sysadmin
63 +
64 # -----------------------------------------------ESXI host--------------------------------------------------------------
65
66 template: vsphere_host_cpu_utilization
67 on: vsphere.host_cpu_utilization
68 class: Utilization
41 - type: Virtual Machine
69 + type: System
70 component: CPU
71 lookup: average -10m unaligned match-names of used
72 units: %
@@ -53,7 +81,7 @@ component: CPU
81 template: vsphere_host_mem_utilization
82 on: vsphere.host_mem_utilization
83 class: Utilization
56 - type: Virtual Machine
84 + type: System
85 component: Memory
86 calc: $used
87 units: %
@@ -61,6 +89,6 @@ component: Memory
89 warn: $this > (($status >= $WARNING) ? (80) : (90))
90 crit: $this > (($status == $CRITICAL) ? (90) : (98))
91 delay: down 15m multiplier 1.5 max 1h
64 - summary: vSphere ESXi Ram utilization for host ${label:host}
92 + summary: vSphere ESXi memory utilization for host ${label:host}
93 info: Memory utilization ESXi host ${label:host} cluster ${label:cluster} datacenter ${label:datacenter}
94 to: sysadmin