Version: 0.15.0 (x64 MSI), Windows Server 2019
What happens
The auxiliary collector logs a conversion error every now and then. Two occurrences within 38 minutes on an otherwise healthy machine:
2026-08-10 16:09:08: error:D:\a\nscp\nscp\modules\CheckSystem\pdh_thread.cpp:477: Failed to get CPU frequency metrics: Failed to fetch CPU frequency metrics: Failed to convert LoadPercentage to number:80020005: Incompatibilità tra tipi.
2026-08-10 16:47:57: error:D:\a\nscp\nscp\modules\CheckSystem\pdh_thread.cpp:477: Failed to get CPU frequency metrics: Failed to fetch CPU frequency metrics: Failed to convert LoadPercentage to number:80020005: Incompatibilità tra tipi.
0x80020005 is DISP_E_TYPEMISMATCH; the trailing text is the localized system message ("type mismatch" on an Italian locale).
Root cause
Win32_Processor.LoadPercentage is occasionally NULL — WMI does not always have a sample ready for it. row::get_int has no case for that:
long long row::get_int(const std::string &col) const {
...
hr = vValue.ChangeType(VT_I8);
if (SUCCEEDED(hr)) return vValue.llVal;
hr = vValue.ChangeType(VT_UI8);
if (SUCCEEDED(hr)) return vValue.ullVal;
throw wmi_exception(hr, "Failed to convert " + col + " to number");
}
ChangeType from VT_NULL fails for both target types, so it throws. Its sibling row::get_string does handle the case, returning "<NULL>":
if (vValue.vt == VT_NULL) return "<NULL>";
(both in include/win/wmi/wmi_query.cpp, at lines 152-161 and 112 respectively, as of 0.15.0)
Why it matters beyond the log noise
The exception escapes cpu_frequency::read_wmi() half-way through the row, propagates through query_wmi() and fetch(), and is only caught by run_fetch in the collector loop. So one NULL field discards the entire CPU frequency collection for that cycle — clock speeds, core counts, everything — not just the field that was missing.
Nothing is disabled permanently (only WBEM_E_INVALID_QUERY / WBEM_E_NOT_FOUND set fetch_cpu_frequency_ = false), so the next cycle recovers. In this particular case the practical impact is small, since these values feed check_cpu_frequency and check_cpu reads from PDH instead.
But get_int is not specific to this collector: it has around 79 call sites across the modules — check_disk_io, check_storagepool, check_shadowcopy, check_share and others — and every one of them turns an optional WMI field into a failed collection. Several of those classes have fields that are legitimately NULL depending on hardware and configuration.
Suggested fix
Whatever the shape, the goal is that a NULL field should not cost the caller the whole row.
The safest option looks like an explicit accessor for optional values, e.g. get_int(col, default) or a get_int_opt() returning boost::optional<long long>, leaving today's get_int throwing for fields that really are mandatory. Callers that read optional fields (LoadPercentage among them) would then opt in.
Just returning 0 from get_int on VT_NULL would also stop the bleeding, but for LoadPercentage zero is a perfectly valid reading, so the data would silently become wrong rather than absent — that is why I would rather see it be explicit at the call site.
Side note
The localized part of the message comes through mangled in the log (Incompatibilità renders with a stray ?? at the end of the line). Probably an encoding conversion on the system error string; happy to open a separate issue if that is worth tracking on its own.
Version: 0.15.0 (x64 MSI), Windows Server 2019
What happens
The auxiliary collector logs a conversion error every now and then. Two occurrences within 38 minutes on an otherwise healthy machine:
0x80020005isDISP_E_TYPEMISMATCH; the trailing text is the localized system message ("type mismatch" on an Italian locale).Root cause
Win32_Processor.LoadPercentageis occasionally NULL — WMI does not always have a sample ready for it.row::get_inthas no case for that:ChangeTypefromVT_NULLfails for both target types, so it throws. Its siblingrow::get_stringdoes handle the case, returning"<NULL>":(both in
include/win/wmi/wmi_query.cpp, at lines 152-161 and 112 respectively, as of 0.15.0)Why it matters beyond the log noise
The exception escapes
cpu_frequency::read_wmi()half-way through the row, propagates throughquery_wmi()andfetch(), and is only caught byrun_fetchin the collector loop. So one NULL field discards the entire CPU frequency collection for that cycle — clock speeds, core counts, everything — not just the field that was missing.Nothing is disabled permanently (only
WBEM_E_INVALID_QUERY/WBEM_E_NOT_FOUNDsetfetch_cpu_frequency_ = false), so the next cycle recovers. In this particular case the practical impact is small, since these values feedcheck_cpu_frequencyandcheck_cpureads from PDH instead.But
get_intis not specific to this collector: it has around 79 call sites across the modules —check_disk_io,check_storagepool,check_shadowcopy,check_shareand others — and every one of them turns an optional WMI field into a failed collection. Several of those classes have fields that are legitimately NULL depending on hardware and configuration.Suggested fix
Whatever the shape, the goal is that a NULL field should not cost the caller the whole row.
The safest option looks like an explicit accessor for optional values, e.g.
get_int(col, default)or aget_int_opt()returningboost::optional<long long>, leaving today'sget_intthrowing for fields that really are mandatory. Callers that read optional fields (LoadPercentageamong them) would then opt in.Just returning
0fromget_intonVT_NULLwould also stop the bleeding, but forLoadPercentagezero is a perfectly valid reading, so the data would silently become wrong rather than absent — that is why I would rather see it be explicit at the call site.Side note
The localized part of the message comes through mangled in the log (
Incompatibilitàrenders with a stray??at the end of the line). Probably an encoding conversion on the system error string; happy to open a separate issue if that is worth tracking on its own.