diff --git a/collector/os.go b/collector/os.go index fb9f8f14..9c9c6af0 100644 --- a/collector/os.go +++ b/collector/os.go @@ -4,9 +4,14 @@ package collector import ( "errors" + "fmt" "time" "github.com/StackExchange/wmi" + "github.com/prometheus-community/windows_exporter/headers/custom" + "github.com/prometheus-community/windows_exporter/headers/netapi32" + "github.com/prometheus-community/windows_exporter/headers/psapi" + "github.com/prometheus-community/windows_exporter/headers/sysinfoapi" "github.com/prometheus-community/windows_exporter/log" "github.com/prometheus/client_golang/prometheus" ) @@ -157,18 +162,30 @@ func (c *OSCollector) collect(ch chan<- prometheus.Metric) (*prometheus.Desc, er return nil, errors.New("WMI query returned empty result set") } + product, buildNum := custom.GetProductDetails() + + nwgi, _, err := netapi32.NetWkstaGetInfo() + if err != nil { + return nil, err + } + ch <- prometheus.MustNewConstMetric( c.OSInformation, prometheus.GaugeValue, 1.0, - dst[0].Caption, - dst[0].Version, + fmt.Sprintf("Microsoft %v", product), // Caption + fmt.Sprintf("%v.%v.%v", nwgi.Wki102_ver_major, nwgi.Wki102_ver_minor, buildNum), // Version ) + gmse, err := sysinfoapi.GlobalMemoryStatusEx() + if err != nil { + return nil, err + } + ch <- prometheus.MustNewConstMetric( c.PhysicalMemoryFreeBytes, prometheus.GaugeValue, - float64(dst[0].FreePhysicalMemory*1024), // KiB -> bytes + float64(gmse.UllAvailPhys), ) currentTime := time.Now() @@ -191,55 +208,71 @@ func (c *OSCollector) collect(ch chan<- prometheus.Metric) (*prometheus.Desc, er ch <- prometheus.MustNewConstMetric( c.PagingFreeBytes, prometheus.GaugeValue, - float64(dst[0].FreeSpaceInPagingFiles*1024), // KiB -> bytes + float64(dst[0].FreeSpaceInPagingFiles*1024), + // Cannot find a way to get this without WMI. + // Can get from CIM_OperatingSystem which is where WMI gets it from, but I can't figure out how to access this from cimwin32.dll + // https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/cim-operatingsystem#properties ) ch <- prometheus.MustNewConstMetric( c.VirtualMemoryFreeBytes, prometheus.GaugeValue, - float64(dst[0].FreeVirtualMemory*1024), // KiB -> bytes + float64(gmse.UllAvailPageFile), ) + // Windows has no defined limit, and is based off available resources. This currently isn't calculated by WMI and is set to default value. + // https://techcommunity.microsoft.com/t5/windows-blog-archive/pushing-the-limits-of-windows-processes-and-threads/ba-p/723824 + // https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-operatingsystem ch <- prometheus.MustNewConstMetric( c.ProcessesLimit, prometheus.GaugeValue, - float64(dst[0].MaxNumberOfProcesses), + float64(4294967295), ) ch <- prometheus.MustNewConstMetric( c.ProcessMemoryLimitBytes, prometheus.GaugeValue, - float64(dst[0].MaxProcessMemorySize*1024), // KiB -> bytes + float64(gmse.UllTotalVirtual), ) + gpi, err := psapi.GetLPPerformanceInfo() + if err != nil { + return nil, err + } + ch <- prometheus.MustNewConstMetric( c.Processes, prometheus.GaugeValue, - float64(dst[0].NumberOfProcesses), + float64(gpi.ProcessCount), ) ch <- prometheus.MustNewConstMetric( c.Users, prometheus.GaugeValue, - float64(dst[0].NumberOfUsers), + float64(nwgi.Wki102_logged_on_users), ) + fsipf, err := custom.GetSizeStoredInPagingFiles() + if err != nil { + return nil, err + } + ch <- prometheus.MustNewConstMetric( c.PagingLimitBytes, prometheus.GaugeValue, - float64(dst[0].SizeStoredInPagingFiles*1024), // KiB -> bytes + float64(fsipf), ) ch <- prometheus.MustNewConstMetric( c.VirtualMemoryBytes, prometheus.GaugeValue, - float64(dst[0].TotalVirtualMemorySize*1024), // KiB -> bytes + float64(gmse.UllTotalPageFile), ) ch <- prometheus.MustNewConstMetric( c.VisibleMemoryBytes, prometheus.GaugeValue, - float64(dst[0].TotalVisibleMemorySize*1024), // KiB -> bytes + float64(gmse.UllTotalPhys), ) return nil, nil diff --git a/headers/custom/custom.go b/headers/custom/custom.go new file mode 100644 index 00000000..752b33ec --- /dev/null +++ b/headers/custom/custom.go @@ -0,0 +1,54 @@ +package custom + +import ( + "log" + "os" + "strings" + + "golang.org/x/sys/windows/registry" +) + +// GetSizeStoredInPagingFiles returns the total size of paging files across all discs. +func GetSizeStoredInPagingFiles() (int64, error) { + k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management`, registry.QUERY_VALUE) + if err != nil { + return 0, err + } + pagingFiles, _, err := k.GetStringsValue("ExistingPageFiles") + if err != nil { + return 0, err + } + + var size int64 = 0 + for _, pagingFile := range pagingFiles { + fileString := strings.ReplaceAll(pagingFile, `\??\`, "") + file, err := os.Stat(fileString) + if err != nil { + return 0, err + } + size += file.Size() + } + return size, nil +} + +// GetProductDetails returns the ProductName and CurrentBuildNumber values from the registry. +func GetProductDetails() (string, string) { + k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) + if err != nil { + log.Fatal(err) + } + pn, _, err := k.GetStringValue("ProductName") + if err != nil { + log.Fatal(err) + } + + bn, _, err := k.GetStringValue("CurrentBuildNumber") + if err != nil { + log.Fatal(err) + } + + if err := k.Close(); err != nil { + log.Fatal(err) + } + return pn, bn +} diff --git a/headers/netapi32/netapi32.go b/headers/netapi32/netapi32.go new file mode 100644 index 00000000..d7605c04 --- /dev/null +++ b/headers/netapi32/netapi32.go @@ -0,0 +1,89 @@ +package netapi32 + +import ( + "errors" + "unsafe" + + "golang.org/x/sys/windows" +) + +// WKSTAInfo102 is a wrapper of WKSTA_Info_102 +//https://docs.microsoft.com/en-us/windows/win32/api/lmwksta/ns-lmwksta-wksta_info_102 +type WKSTAInfo102 struct { + Wki102_platform_id uint32 + wki102_computername *uint16 + wki102_langroup *uint16 + Wki102_ver_major uint32 + Wki102_ver_minor uint32 + wki102_lanroot *uint16 + Wki102_logged_on_users uint32 +} + +var ( + netapi32 = windows.NewLazySystemDLL("netapi32") + procNetWkstaGetInfo = netapi32.NewProc("NetWkstaGetInfo") + procNetApiBufferFree = netapi32.NewProc("NetApiBufferFree") +) + +// NetApiStatus is a map of Network Management Error Codes. +// https://docs.microsoft.com/en-gb/windows/win32/netmgmt/network-management-error-codes?redirectedfrom=MSDN +var NetApiStatus = map[uint32]string{ + // Success + 0: "NERR_Success", + // This computer name is invalid. + 2351: "NERR_InvalidComputer", + // This operation is only allowed on the primary domain controller of the domain. + 2226: "NERR_NotPrimary", + /// This operation is not allowed on this special group. + 2234: "NERR_SpeGroupOp", + /// This operation is not allowed on the last administrative account. + 2452: "NERR_LastAdmin", + /// The password parameter is invalid. + 2203: "NERR_BadPassword", + /// The password does not meet the password policy requirements. + /// Check the minimum password length, password complexity and password history requirements. + 2245: "NERR_PasswordTooShort", + /// The user name could not be found. + 2221: "NERR_UserNotFound", + // Errors + 5: "ERROR_ACCESS_DENIED", + 8: "ERROR_NOT_ENOUGH_MEMORY", + 87: "ERROR_INVALID_PARAMETER", + 123: "ERROR_INVALID_NAME", + 124: "ERROR_INVALID_LEVEL", + 234: "ERROR_MORE_DATA", + 1219: "ERROR_SESSION_CREDENTIAL_CONFLICT", +} + +// NetApiBufferFree frees the memory other network management functions use internally to return information. +// https://docs.microsoft.com/en-us/windows/win32/api/lmapibuf/nf-lmapibuf-netapibufferfree +func NetApiBufferFree(buffer *WKSTAInfo102) { + procNetApiBufferFree.Call(uintptr(unsafe.Pointer(buffer))) +} + +// NetWkstaGetInfo returns information about the configuration of a workstation. +// https://docs.microsoft.com/en-us/windows/win32/api/lmwksta/nf-lmwksta-netwkstagetinfo +func NetWkstaGetInfo() (WKSTAInfo102, uint32, error) { + // Struct + var lpwi *WKSTAInfo102 + pLpwi := uintptr(unsafe.Pointer(&lpwi)) + + // Null value + var nullptr = uintptr(0) + + // Level + pLevel := uintptr(102) + + // Func call + r1, _, _ := procNetWkstaGetInfo.Call(nullptr, pLevel, pLpwi) + + if ret := *(*uint32)(unsafe.Pointer(&r1)); ret != 0 { + return WKSTAInfo102{}, ret, errors.New(NetApiStatus[ret]) + } + + // Derence the pointer and return the object so we can safely clear the buffer. + var deref WKSTAInfo102 = *lpwi + defer NetApiBufferFree(lpwi) + + return deref, 0, nil +} diff --git a/headers/psapi/psapi.go b/headers/psapi/psapi.go new file mode 100644 index 00000000..c16a42e4 --- /dev/null +++ b/headers/psapi/psapi.go @@ -0,0 +1,98 @@ +package psapi + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +// LPPerformanceInformation is a wrapper of the PERFORMANCE_INFORMATION struct. +// https://docs.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-performance_information +type LPPerformanceInformation struct { + cb uint32 + CommitTotal *uint32 + CommitLimit *uint32 + CommitPeak *uint32 + PhysicalTotal *uint32 + PhysicalAvailable *uint32 + SystemCache *uint32 + KernelTotal *uint32 + KernelPaged *uint32 + KernelNonpaged *uint32 + PageSize *uint32 + HandleCount uint32 + ProcessCount uint32 + ThreadCount uint32 +} + +// PerformanceInformation is a dereferenced version of LPPerformanceInformation +type PerformanceInformation struct { + cb uint32 + CommitTotal uint32 + CommitLimit uint32 + CommitPeak uint32 + PhysicalTotal uint32 + PhysicalAvailable uint32 + SystemCache uint32 + KernelTotal uint32 + KernelPaged uint32 + KernelNonpaged uint32 + PageSize uint32 + HandleCount uint32 + ProcessCount uint32 + ThreadCount uint32 +} + +var ( + psapi = windows.NewLazySystemDLL("psapi.dll") + procGetPerformanceInfo = psapi.NewProc("GetPerformanceInfo") +) + +// GetLPPerformanceInfo retrieves the performance values contained in the LPPerformanceInformation structure. +// https://docs.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getperformanceinfo +func GetLPPerformanceInfo() (LPPerformanceInformation, error) { + var pi LPPerformanceInformation + size := (uint32)(unsafe.Sizeof(pi)) + pi.cb = size + pPi := uintptr(unsafe.Pointer(&pi)) + r1, _, err := procGetPerformanceInfo.Call(pPi, uintptr(size)) + + if ret := *(*bool)(unsafe.Pointer(&r1)); ret == false { + // returned false + return LPPerformanceInformation{}, err + } + + return pi, nil +} + +// GetPerformanceInfo returns the dereferenced version of GetLPPerformanceInfo. +func GetPerformanceInfo() (PerformanceInformation, error) { + var lppi LPPerformanceInformation + size := (uint32)(unsafe.Sizeof(lppi)) + lppi.cb = size + pLppi := uintptr(unsafe.Pointer(&lppi)) + r1, _, err := procGetPerformanceInfo.Call(pLppi, uintptr(size)) + + if ret := *(*bool)(unsafe.Pointer(&r1)); ret == false { + // returned false + return PerformanceInformation{}, err + } + + var pi PerformanceInformation + pi.cb = lppi.cb + pi.CommitTotal = *(*uint32)(unsafe.Pointer(&lppi.CommitTotal)) + pi.CommitLimit = *(*uint32)(unsafe.Pointer(&lppi.CommitLimit)) + pi.CommitPeak = *(*uint32)(unsafe.Pointer(&lppi.CommitPeak)) + pi.PhysicalTotal = *(*uint32)(unsafe.Pointer(&lppi.PhysicalTotal)) + pi.PhysicalAvailable = *(*uint32)(unsafe.Pointer(&lppi.PhysicalAvailable)) + pi.SystemCache = *(*uint32)(unsafe.Pointer(&lppi.SystemCache)) + pi.KernelTotal = *(*uint32)(unsafe.Pointer(&lppi.KernelTotal)) + pi.KernelPaged = *(*uint32)(unsafe.Pointer(&lppi.KernelPaged)) + pi.KernelNonpaged = *(*uint32)(unsafe.Pointer(&lppi.KernelNonpaged)) + pi.PageSize = *(*uint32)(unsafe.Pointer(&lppi.PageSize)) + pi.HandleCount = lppi.HandleCount + pi.ProcessCount = lppi.ProcessCount + pi.ThreadCount = lppi.ThreadCount + + return pi, nil +} diff --git a/headers/sysinfoapi/sysinfoapi.go b/headers/sysinfoapi/sysinfoapi.go new file mode 100644 index 00000000..d9c29202 --- /dev/null +++ b/headers/sysinfoapi/sysinfoapi.go @@ -0,0 +1,43 @@ +package sysinfoapi + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +// MemoryStatusEx is a wrapper for MEMORYSTATUSEX +// https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/ns-sysinfoapi-memorystatusex +type MemoryStatusEx struct { + dwLength uint32 + DwMemoryLoad uint32 + UllTotalPhys uint64 + UllAvailPhys uint64 + UllTotalPageFile uint64 + UllAvailPageFile uint64 + UllTotalVirtual uint64 + UllAvailVirtual uint64 + UllAvailExtendedVirtual uint64 +} + +var ( + kernel32 = windows.NewLazySystemDLL("kernel32.dll") + procGetSystemInfo = kernel32.NewProc("GetSystemInfo") + procGlobalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx") +) + +// GlobalMemoryStatusEx retrieves information about the system's current usage of both physical and virtual memory. +// https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-globalmemorystatusex +func GlobalMemoryStatusEx() (MemoryStatusEx, error) { + var mse MemoryStatusEx + mse.dwLength = (uint32)(unsafe.Sizeof(mse)) + pMse := uintptr(unsafe.Pointer(&mse)) + r1, _, err := procGlobalMemoryStatusEx.Call(pMse) + + if ret := *(*bool)(unsafe.Pointer(&r1)); ret == false { + // returned false + return MemoryStatusEx{}, err + } + + return mse, nil +}