Skip to content

Windows server roles get their own checks, and check messages finally read like numbers

0.17.0 adds twelve new checks — eight for IIS and Remote Desktop Services on Windows, four for the status pages of the common web servers — and gives every filter check control over how it renders numbers, so 140.293GB/0.983TB can become 141.09GB/1006.85GB (or 141,09GB/1.006,85GB). Alongside that, the filter engine stops quietly doing the wrong thing: text-versus-number comparisons are numeric, fractional thresholds mean what they say, and an error inside a syntax template is reported instead of rendering a blank.

Highlights

  • Eight new Windows checks for IIS and Remote Desktop Services. The new CheckWindowsApps module covers IIS sites, application pools, worker processes and HTTP.sys request queues, plus RDS CAL licensing, session counts, per-session load and the Connection Broker counterset.
  • Four new web-server status checks. check_apache_status, check_nginx_status, check_phpfpm_status and check_tomcat_status read the vendors’ machine-readable status endpoints over HTTP(S), sharing check_http’s auth and TLS handling.
  • Check messages can be told how to render their numbers. Four new options — decimals, byte-unit, decimal-separator, thousands-separator — on every filter check and every real-time filter (#1428). Perfdata and thresholds are untouched.
  • Filter comparisons between text and a bare number are now numeric. filter=value > 90 no longer matches value=100 as false because “100” sorts before “90”, and 90 > value evaluates at all.
  • Fractional thresholds stop being truncated. count > 2.5 meant count > 3 and working_set > 1.5g meant 1g; both now mean what they say.
  • Host name placeholders resolve across the whole settings subsystem — including attachment targets and [/includes] (#458) — and are sanitized before they land in a local path 🔒.
  • Syslog submission works again after ten years. SyslogClient read its connection settings from the wrong place and sent nothing at all; a configured syslog target will start receiving traffic on upgrade.
  • Check-specific filter keywords that shadowed the generic summary keywords are renamed, with the old names kept as deprecated aliases.

Detailed changes

CheckWindowsApps — a new module for Windows server roles

A new Windows-only module carrying IIS and Remote Desktop Services checks, built on the performance counter sets and enriched from WMI where the role’s provider is installed. The two roles share one module deliberately: every check module statically links Boost and the filter engine, so a role earns its own DLL only when it drags in a heavy or optional dependency (the way CheckMySQL carries libmariadb.dll).

Command Reports
check_iis_app_pools Per-pool state, uptime and recycles. CRITICAL by default when an auto-start pool is not running; a pool that has never started since boot surfaces as unknown rather than hiding.
check_iis_sites Per-site state, connections and uptime, plus requests_per_sec / bytes_per_sec behind averages=true. CRITICAL by default when an auto-start site is stopped.
check_iis_worker_processes Per-w3wp active and served requests, with the <pid>_<pool> instance name split into keywords. An empty set is OK — idle pools spin their workers down.
check_iis_request_queues Per-queue length, rejections and age, defaulting to HTTP.sys’ 1000-request limit (warn > 800, critical > 1000).
check_rds_licenses One record per CAL key pack from Win32_TSLicenseKeyPack: total, issued and available licences. Warns at available < 10 and total > 0, critical at available = 0 and total > 0.
check_rds_sessions Active, inactive and total session counts, all three as perfdata. No default thresholds — the interesting limits are per-farm.
check_rds_session_load One record per session (Console, Services, RDP-Tcp <n>) with CPU, working set and, on session hosts, RDP protocol bytes — the per-user attribution check_process cannot give. sessions-only=true skips the session-0 aggregate.
check_rds_broker The Connection Broker counterset. Counter names vary between Windows Server versions, so the check enumerates whatever the counterset exposes and reports one record per counter instead of hard-coding names.

A host without the role gets a clean UNKNOWN naming the missing role, not a WMI or PDH error dump. The counter plumbing landed as a reusable gather helper that collects a set of English counter names for every instance of an object in one query, keeping the existing localized/English/index resolution fallback — it was verified against live Swedish-localized counters.

CheckNet — status-page checks for the common web servers

Command Endpoint Keywords
check_apache_status mod_status (?auto appended automatically) workers, requests/s, scoreboard
check_nginx_status stub_status active/reading/writing/waiting, cumulative accepts/handled/requests, derived dropped count
check_phpfpm_status FPM status page processes, listen queue, max_children_reached, slow requests; warns by default when requests queue up
check_tomcat_status manager status?XML=true (appended automatically) per-connector thread pool, request/error counters, JVM heap; defaults fire at 75%/90% pool usage

All four share check_http’s connection handling — Basic auth, TLS version / verify / CA, timeout — and go CRITICAL by default when the endpoint is unreachable, answers non-2xx, or serves something that is not the expected status format. Numeric parsing pins the classic locale, so a host with a decimal comma no longer truncates ReqPerSec at the decimal point.

Filter messages — configurable number rendering

check_drivesize reported 140.293GB/0.983TB used: two units and six decimals in one line, with no way to change either (#1428). Every filter check now takes four options, and real-time filters take the same values as settings keys (decimals, byte unit, decimal separator, thousands separator), inheritable from the default template.

Option Effect
decimals Exactly N decimals. Default -1 keeps the historical “up to three, trailing zeros stripped”. Capped at 15.
byte-unit Pin every byte value to one unit, BEB.
decimal-separator Radix character — , for the European rendering.
thousands-separator Digit grouping for the integer part.
check_drivesize drive=/ show-all=true decimals=2 byte-unit=GB
OK /: 141.09GB/1006.85GB used

check_drivesize drive=/ show-all=true decimals=2 byte-unit=GB \
  decimal-separator=, thousands-separator=.
OK /: 141,09GB/1.006,85GB used

The format lives on the evaluation context, so it reaches the message only: performance data is built from the raw values and keeps its full precision and its . radix, and so does every number the filter grammar parses out of a threshold — warning=used>1.5g means the same thing with a decimal comma in force.

Three defects in the byte formatter came out of this work:

  • format_bytes(used, 'gb') rendered 1.27055e-10, because the unit comparison was case sensitive against an uppercase table. Units are now case insensitive everywhere.
  • A unit that matched nothing fell out of the comparison having divided seven times, rendering value/1024^7. An unknown unit is now reported — Filter processing failed: format_bytes failed: Unknown byte unit: ZB — and the same check applies inside real-time filters.
  • format_bytes(value, '') failed to parse at all; the empty string literal is now accepted.

The filter/where engine — comparisons that mean what they say

  • Text keyword versus bare number is numeric. A string-typed keyword compared against an unquoted number used to order lexically, or — with the operands reversed — fail to evaluate. Both sides now compare as numbers. This covers value/warn/crit/min/max (filter_perf, render_perf), speed (check_network), string_value (check_registry_value) and column() (check_logfile). A value that is not a number never matches; the check logs one warning naming it and stays a certain non-match, not UNKNOWN. Quoted literals keep the lexical comparison, as do like, regexp, in, keyword-specific converters (state = 'running', age > 30m) and the = 'unknown' / = 'never' sentinels.
  • Fractional numbers survive. count > 2.5 used to be rounded into the counter’s integer domain, and unit literals lost their fraction entirely (working_set > 1.5g meant 1g, uptime < 2.5h meant 2h).
  • filter_perf/render_perf/xform_perf: max and min were swapped. max read the perf-data minimum bound and min the maximum; they now read the bounds they name.
  • Template errors are reported. A function that failed inside detail-syntax or top-syntax left the placeholder empty and said nothing; the check now returns UNKNOWN with Filter processing failed: ….
  • perf-config’s unit: converts instead of relabelling. On byte series that do not auto-scale, unit:KB used to change the label only, shipping =1536KB for 1536 bytes. The value and the warn/crit bounds now convert. An unrecognised unit leaves the value alone rather than dividing it by 1024⁷.

Filter keywords — the clash with the generic summary keywords is resolved

A handful of checks registered a keyword named status, count or total — the same names as the built-in summary keywords. The check-specific value won in filter/warning/critical and detail-syntax, while top-syntax and the reference documentation showed the generic one. Each now has a distinct name:

Check Old New
check_cpu, check_cpu_utilization total usage
check_battery status battery_status
check_network status, total link_status, throughput
check_os_updates count updates
check_patch_age count patches
check_pending_reboot count signals
check_printjobs status job_status
check_printqueue status printer_status
check_installed_software (Linux) status package_status
check_activation status activation_status
check_docker status container_status
check_connections count, total connections, total_connections
check_dns count records
check_http status status_message
check_shadowcopy count copies
check_disk_health total size

The old names remain as undocumented deprecated aliases with unchanged behaviour, so check_cpu "warn=total > 80" still works.

Settings — host name placeholders, and where they may land

${host}, ${hostname}, ${hostname_lc}, ${hostname_uc} and ${domain} now resolve in attachment target paths and in [/includes], not only in settings urls and the url an attachment is fetched from (#458). An unknown ${...} token in a path is not an error — it resolves to the installation directory — so a configuration like [/attachments] ${shared-path}/${host}.ini = … never failed, it quietly wrote one file with the installation directory in its name.

🔒 Because the host name is not fully under the operator’s control (DHCP, or any local privileged process can set it), a value substituted into a path is reduced to the characters a legal RFC-952 host name can contain: anything else becomes _, and a dots-only value becomes _. Settings urls and the submit clients’ host name specs are unaffected. See Security notices.

nscp settings --migrate-to (and the REST migrate) now keeps a placeholder you pass it as-is in boot.ini while migrating into the expanded per-host file, the way --switch already did, so the template survives on a fleet-managed machine.

Clients — submission paths that were quietly dead

  • Syslog. SyslogClient read its connection settings from the sender rather than the target, so address, port, facility, severity and templates were all ignored: the agent logged Undefined facility: and sent nothing. Broken since 0.4.3 (2015). CheckMKClient had the same defect on its query path.
  • SMTP. The sender’s host name was read from the wrong place, so it was always empty and the EHLO fell back to localhost. Set ehlo-hostname on the target if your mail server applies HELO/EHLO policy.
  • Short command names. A client command shorter than eight characters — cpu, run — answered Exception processing command line: basic_string::substr … instead of running, in every module built on the shared client machinery (NRPE, NSCA, NRDP, Graphite, …).

CheckSystem — check_pending_reboot says since when

The CBS and Windows Update reboot keys exist only while their reboot is queued, so their last-write time is when the signal appeared. Two new keywords follow check_registry’s naming: written (type_date, plus written_s) and age, duration-typed so warning=pending = 1 and age > 7d reads as seven days. The default message gains (pending since <time>) when the time is known (#1415). The file-rename, computer-rename and domain-join signals carry no timestamp, so both keywords are optional: they render as unknown, compare false against every number and emit no perfdata rather than reporting a misleading value.

Data collection — one bad field no longer sinks the cycle

  • WMI. Win32_Processor.LoadPercentage is occasionally NULL, and row::get_int had no case for it: the type-mismatch exception escaped half-way through the row and the collector threw away the entire cycle’s clock speeds and core counts (#1391). Optional fields can now opt into boost::none, mandatory ones fail with a clear <col> is NULL instead of localized COM text, and check_cpu_frequency renders a missing sample as no load sample rather than a fabricated 0.
  • PDH. MaxQueueItemAge on an idle, freshly started queue returns PDH_CALC_NEGATIVE_DENOMINATOR, which failed a whole gather even when the caller passed ignore_errors — making check_iis_request_queues misreport the object as missing. With ignore_errors the counter is now skipped for that tick (#642, #906); the background collector and every single-counter check still throw, so they hear about an uncomputable counter instead of silently reading a default.

Windows installer and file layout

A round of fixes to the modern (ProgramData) layout introduced in 0.16.2: upgrading an enrolled host resolves every path token instead of failing; ReadLayout gets the install folder before directories resolve; CURRENT_LAYOUT is set through the public property setter; an upgrade of a modern host no longer re-creates nsclient.ini in Program Files; a %ProgramData% that cannot be resolved fails outright instead of half-applying the layout; migrated files get the destination’s ACL rather than the one they came with; resetting a renamed tree to inherited strips the explicit ACEs; and --migrate-layout legacy migrates to legacy instead of silently to modern.

Bug fixes

  • nscp settings --show --path … without a --key used to print nothing and exit 0; it now reports Invalid command line please use --path and --key with show and exits non-zero.
  • The settings diff behind the REST diff endpoint kept listing an edit for the lifetime of the process after it had been written, reporting a modified entry whose old value equalled its new one.
  • Only the pending markers the backend confirms are dropped on save, and staged deletions are masked in has_key.
  • Boolean option defaults render as true/false in the generated reference instead of garbage.

Documentation

Options shared by every filter check (filter, warning, top-syntax, …) and the generic filter keywords are now single-sourced: they fold out of each command’s reference page into one shared page, so a command’s documentation shows only what is specific to it. Runtime-stubbed Windows-only checks are marked as Windows only.

Upgrade notes

  • Syslog starts delivering. If you have a syslog target configured, check it still points where you want before upgrading — it has not been delivering, and it will now. The same applies to SMTP targets, which will start announcing this host in EHLO instead of localhost.
  • Host name placeholders in paths now resolve. Check any ${host}, ${hostname} or ${domain} under [/attachments] or [/includes] and remove workarounds — such a file lands somewhere new after upgrade. 🔒 The value is sanitized when it lands in a local path. Configurations without a host name placeholder are unaffected.
  • Number rendering is opt-in, but it is all-or-nothing per check. Leave all four options unset and messages are byte-for-byte unchanged. Set any of them and plain float keywords move onto the number format too: with decimals unset they render with up to three decimals instead of the legacy 6-significant-digit form (2.710942.711), and large values stop rendering scientific. A pipeline that matches float text in the message may need its pattern relaxed.
  • An unknown unit in format_bytes() now returns UNKNOWN instead of a quietly wrong number. A syntax string with a typo’d unit will fail until the unit is fixed.
  • perf-config unit: on plain byte series changes the metric’s magnitude. A dashboard that compensated for the old mislabelling will see the metric drop by the unit ratio; a graph flat at a near-zero value because of a misspelled unit: will jump to its real magnitude.
  • Filter comparisons against a bare number are numeric. Review any filter that deliberately relied on text ordering — quote the number to keep the old behaviour.
  • Fractional thresholds change meaning. Whole-number thresholds are unchanged; expressions that already used a decimal point can behave differently.
  • max and min in filter_perf/render_perf/xform_perf were swapped. A filter that compensated needs the two names exchanged back.
  • Renamed filter keywords keep working through deprecated aliases, but three default perfdata keys change because the default perf-config names the renamed keyword: check_cpu_utilization (Linux) cpu_totalcpu_usage, check_patch_age patch_countpatch_patches, check_pending_reboot reboot_countreboot_signals. Pass your own perf-config=extra(...) with the old name to keep the old key. check_os_updates’ default output now reports the actual number of updates instead of the matched-row count.
  • check_pending_reboot’s default message gains a suffixReboot required: Windows Update (pending since 2026-08-16 09:41:12). Notification pipelines matching the exact message text need their pattern relaxed.
  • nscp settings --show without --key now fails. Scripts relying on the silent success need the missing --key added.

Download

You can download the new version from GitHub

// Michael Medin