Performance Counter (PDH) Monitoring¶
Goal: Read a Windows Performance Counter (PDH) value, alert on it, and (optionally) check it against a moving average instead of a single instantaneous sample.
PDH (Performance Data Helper) is the legacy Windows performance API. It is older than WMI and has real warts — counters are localised, sometimes vanish from the registry, and rate counters need two samples to mean anything — but it remains the only way to read many useful metrics, and once you know the gotchas it’s straightforward.
Prerequisites¶
Enable the CheckSystem module in nsclient.ini:
[/modules]
CheckSystem = enabled
NRPEServer = enabled ; if using NRPE
Basic Counter Check¶
Command¶
check_pdh "counter=\\Processor(_Total)\\% Processor Time"
Expected output (healthy)¶
OK: \\Processor(_Total)\\% Processor Time = 4
'\\Processor(_Total)\\% Processor Time'=4;0;0
The counter path uses backslashes; on Unix shells you’ll need to double-escape them.
Common Scenarios¶
Find a counter (when you don’t know the exact name)¶
NSClient++ ships a built-in PDH browser. Run it from a command prompt on the Windows host:
nscp sys -- --list --all
Filter for keywords:
nscp sys -- --list Disk --all
Add --filter to narrow that down — every filter has to match, and it is
matched against the whole \object(instance)\counter path, so one can name the
object and the next the counter. Repeat it as often as you need:
nscp sys -- --list Disk --all --filter Queue --filter Avg.
\LogicalDisk(C:)\Avg. Disk Queue Length
\LogicalDisk(C:)\Avg. Disk Read Queue Length
\LogicalDisk(C:)\Avg. Disk Write Queue Length
Matching is a plain substring test and ignores case, so --list disk finds the
same counters as --list Disk — you are looking for a name you do not know yet,
and PDH capitalises its own inconsistently. --no-instances leaves the
per-instance expansion out, which is what you want while you are still hunting
for the counter name.
List instances of a counter that has them (e.g. per-disk, per-network-interface):
nscp sys -- --expand-path "\\PhysicalDisk(*)\\Avg. Disk Queue Length"
Validate a counter actually returns data¶
Before wiring a counter into a Nagios check, verify it on the host:
nscp sys -- --validate Disk --all
\\PhysicalDisk(_Total)\\Avg. Disk Bytes/Read: ok-rate(0)
\\PhysicalDisk(_Total)\\% Idle Time: ok-rate(0)
ok-rate(N) means the counter responded; N is the sample value. If a counter is broken, the
output flags it here rather than at check time.
Look up an index ⇄ name (helpful for localisation troubleshooting)¶
nscp sys -- --lookup-index 238 ; index → English name
nscp sys -- --lookup-name "Processor" ; name → index
Alert on a single counter¶
check_pdh "counter=\\PhysicalDisk(_Total)\\Avg. Disk Queue Length" \
"warn=value > 2" "crit=value > 5"
Force min/max bounds for graphing¶
PDH doesn’t tell NSClient++ what range a counter lives in. For percentages or known-bounded counters, declare them yourself so graphing systems can auto-scale:
check_pdh "counter=\\Processor(_Total)\\% Processor Time" \
"perf-config=*(minimum:0;maximum:100)"
See Checks In Depth: Performance Data for the full
perf-config reference.
Predefined Counters (averages over time)¶
A check that fires once a minute returns one instantaneous sample. For a noisy counter (CPU, queue length, network rates) you usually want an average over the last N seconds.
Predefine the counter in the config file with collection strategy = rrd:
[/settings/system/windows/counters/disk_q]
collection strategy = rrd
counter = \\PhysicalDisk(_Total)\\Avg. Disk Write Queue Length
Then check by name, asking for the average over the last 30 seconds:
check_pdh counter=disk_q time=30s
The named-counter form also sidesteps shell-escaping pain (the counter path is in the config file, which is plain UTF-8) and lets you reuse the same definition from many checks.
Describing a counter for Prometheus¶
Every predefined counter is also published on
/api/v2/openmetrics, as
system_metrics_pdh_<name>. Nobody but you knows what
\\PhysicalDisk(_Total)\\Avg. Disk Write Queue Length measures, so the counter
definition is where you say:
[/settings/system/windows/counters/disk_q]
collection strategy = rrd
counter = \\PhysicalDisk(_Total)\\Avg. Disk Write Queue Length
help = Average write queue length across all physical disks
unit =
| Key | Effect |
|---|---|
help |
Becomes the # HELP line of the metric. Defaults to the counter path, which is better than nothing but reads like a path. |
unit |
Becomes the # UNIT line, and the metric name is made to end in _<unit>, which OpenMetrics requires of a metric that declares one. Leave it empty for a plain count, a queue length or a per-second rate. |
A unit becomes part of a metric name, so it has to be spelled in
[a-zA-Z0-9_]. Anything else is rewritten the way a metric name is
(bytes/sec becomes bytes_sec), or dropped if nothing usable is left, and
the agent logs which counter it happened to — the metric itself is still
published either way.
Neither key changes what check_pdh returns or what the JSON metrics endpoint
reports — they only describe the counter to a scraper.
When to predefine vs. check by path¶
| Use a named/predefined counter when… | Use a direct path when… |
|---|---|
You want averaging (time=30s etc.) |
One-off ad-hoc check |
| The path has localised / non-ASCII chars | Pure ASCII counter name |
| Many checks use the same counter | Single use |
| You’re hitting shell-quoting headaches | Counter syntax is shell-friendly |
Common Gotchas¶
Counters are localised¶
A counter named \\Processor(_Total)\\% Processor Time on an English server is
\\Procesor(_Total)\\% czas procesora on a Polish one. Two ways to handle this:
- English fallback names — NSClient++ resolves English names against the local registry. This is the simplest fix and works with the path syntax above.
- Index-based lookup — counters have stable numeric indices. Use
nscp sys -- --lookup-nameto find the index, then reference the counter by index.
If a localised counter passes through NRPE/check_nt and gets mangled, set the encoding to match
the upstream (typically UTF-8) under [/settings/default].
Counters sometimes get lost¶
The PDH name registry occasionally corrupts itself — counters that worked yesterday return “counter not found” today. Restore from the system cache:
lodctr /R
(Run as Administrator. Microsoft KB: https://support.microsoft.com/kb/300956.)
Rate counters always read 0¶
Counters whose unit is per-second (e.g. \\PhysicalDisk\\Disk Reads/sec) require two samples —
one read returns nothing meaningful. Add the averages flag to take two samples one second apart:
check_pdh "counter=\\PhysicalDisk(_Total)\\Disk Reads/sec" averages
Wrong value range or capping¶
Some counters are capped (e.g. % Processor Time won’t exceed 100) or scaled internally. To
disable capping/scaling, use the flags argument:
check_pdh "counter=\\Foo\\Bar" flags=nocap100,noscale
Wrong datatype¶
Counters can be reported as long, large (64-bit), or double. NSClient++ defaults to large;
if a counter returns the wrong magnitude, override with type=double (or long).
Via NRPE¶
check_nrpe -H <agent-ip> -c check_pdh -a "counter=\\Processor(_Total)\\% Processor Time" "warn=value > 80" "crit=value > 95"
If the counter path fails through NRPE due to escaping or localisation, predefine the counter on the agent (see above) and check by name:
check_nrpe -H <agent-ip> -c check_pdh -a counter=disk_q time=30s
Next Steps¶
- Windows Server Health — combine PDH counters with CPU/memory/disk baselines
- Checks In Depth: Performance Data — customise perfdata, force min/max bounds for graphing
- Checks In Depth: Filters — write filter and threshold expressions
- FAQ: Failed to open performance counters — rebuilding a broken PDH registry
- Reference: CheckSystem — full
check_pdhcommand reference