diff --git a/collector/cs.go b/collector/cs.go index 1978855c..63e87274 100644 --- a/collector/cs.go +++ b/collector/cs.go @@ -3,10 +3,9 @@ package collector import ( - "errors" - - "github.com/StackExchange/wmi" + "github.com/prometheus-community/windows_exporter/headers/sysinfoapi" "github.com/prometheus-community/windows_exporter/log" + "github.com/prometheus/client_golang/prometheus" ) @@ -60,51 +59,47 @@ func (c *CSCollector) Collect(ctx *ScrapeContext, ch chan<- prometheus.Metric) e return nil } -// Win32_ComputerSystem docs: -// - https://msdn.microsoft.com/en-us/library/aa394102 -type Win32_ComputerSystem struct { - NumberOfLogicalProcessors uint32 - TotalPhysicalMemory uint64 - DNSHostname string - Domain string - Workgroup *string -} - func (c *CSCollector) collect(ch chan<- prometheus.Metric) (*prometheus.Desc, error) { - var dst []Win32_ComputerSystem - q := queryAll(&dst) - if err := wmi.Query(q, &dst); err != nil { + // Get systeminfo for number of processors + systemInfo := sysinfoapi.GetSystemInfo() + + // Get memory status for physical memory + mem, err := sysinfoapi.GlobalMemoryStatusEx() + if err != nil { return nil, err } - if len(dst) == 0 { - return nil, errors.New("WMI query returned empty result set") - } ch <- prometheus.MustNewConstMetric( c.LogicalProcessors, prometheus.GaugeValue, - float64(dst[0].NumberOfLogicalProcessors), + float64(systemInfo.NumberOfProcessors), ) ch <- prometheus.MustNewConstMetric( c.PhysicalMemoryBytes, prometheus.GaugeValue, - float64(dst[0].TotalPhysicalMemory), + float64(mem.TotalPhys), ) - var fqdn string - if dst[0].Workgroup == nil || dst[0].Domain != *dst[0].Workgroup { - fqdn = dst[0].DNSHostname + "." + dst[0].Domain - } else { - fqdn = dst[0].DNSHostname + hostname, err := sysinfoapi.GetComputerName(sysinfoapi.ComputerNameDNSHostname) + if err != nil { + return nil, err + } + domain, err := sysinfoapi.GetComputerName(sysinfoapi.ComputerNameDNSDomain) + if err != nil { + return nil, err + } + fqdn, err := sysinfoapi.GetComputerName(sysinfoapi.ComputerNameDNSFullyQualified) + if err != nil { + return nil, err } ch <- prometheus.MustNewConstMetric( c.Hostname, prometheus.GaugeValue, 1.0, - dst[0].DNSHostname, - dst[0].Domain, + hostname, + domain, fqdn, ) diff --git a/collector/cs_test.go b/collector/cs_test.go new file mode 100644 index 00000000..a4b3c9f1 --- /dev/null +++ b/collector/cs_test.go @@ -0,0 +1,23 @@ +package collector + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" +) + +func BenchmarkCsCollect(b *testing.B) { + c, err := NewCSCollector() + if err != nil { + b.Error(err) + } + metrics := make(chan prometheus.Metric) + go func() { + for { + <-metrics + } + }() + for i := 0; i < b.N; i++ { + c.Collect(&ScrapeContext{}, metrics) + } +} diff --git a/collector/os.go b/collector/os.go index f9db5796..67e01ec6 100644 --- a/collector/os.go +++ b/collector/os.go @@ -3,16 +3,21 @@ package collector import ( - "errors" + "fmt" + "os" + "strings" "time" - "github.com/StackExchange/wmi" - "github.com/prometheus-community/windows_exporter/log" + "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/client_golang/prometheus" + "github.com/prometheus/common/log" + "golang.org/x/sys/windows/registry" ) func init() { - registerCollector("os", NewOSCollector) + registerCollector("os", NewOSCollector, "Paging File") } // A OSCollector is a Prometheus collector for WMI metrics @@ -32,6 +37,12 @@ type OSCollector struct { Timezone *prometheus.Desc } +type pagingFileCounter struct { + Name string + Usage float64 `perflib:"% Usage"` + UsagePeak float64 `perflib:"% Usage Peak"` +} + // NewOSCollector ... func NewOSCollector() (Collector, error) { const subsystem = "os" @@ -121,7 +132,7 @@ func NewOSCollector() (Collector, error) { // Collect sends the metric values for each metric // to the provided prometheus Metric channel. func (c *OSCollector) Collect(ctx *ScrapeContext, ch chan<- prometheus.Metric) error { - if desc, err := c.collect(ch); err != nil { + if desc, err := c.collect(ctx, ch); err != nil { log.Error("failed collecting os metrics:", desc, err) return err } @@ -146,41 +157,102 @@ type Win32_OperatingSystem struct { Version string } -func (c *OSCollector) collect(ch chan<- prometheus.Metric) (*prometheus.Desc, error) { - var dst []Win32_OperatingSystem - q := queryAll(&dst) - if err := wmi.Query(q, &dst); err != nil { +func (c *OSCollector) collect(ctx *ScrapeContext, ch chan<- prometheus.Metric) (*prometheus.Desc, error) { + nwgi, err := netapi32.GetWorkstationInfo() + if err != nil { return nil, err } - if len(dst) == 0 { - return nil, errors.New("WMI query returned empty result set") + gmse, err := sysinfoapi.GlobalMemoryStatusEx() + if err != nil { + return nil, err } + currentTime := time.Now() + timezoneName, _ := currentTime.Zone() + + // Get total allocation of paging files across all disks. + memManKey, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management`, registry.QUERY_VALUE) + defer memManKey.Close() + + if err != nil { + return nil, err + } + pagingFiles, _, err := memManKey.GetStringsValue("ExistingPageFiles") + if err != nil { + return nil, err + } + + // Get build number and product name from registry + ntKey, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) + defer ntKey.Close() + + if err != nil { + return nil, err + } + + pn, _, err := ntKey.GetStringValue("ProductName") + if err != nil { + return nil, err + } + + bn, _, err := ntKey.GetStringValue("CurrentBuildNumber") + if err != nil { + return nil, err + } + + var fsipf float64 = 0 + for _, pagingFile := range pagingFiles { + fileString := strings.ReplaceAll(pagingFile, `\??\`, "") + file, err := os.Stat(fileString) + if err != nil { + return nil, err + } + fsipf += float64(file.Size()) + } + + gpi, err := psapi.GetPerformanceInfo() + if err != nil { + return nil, err + } + + var pfc = make([]pagingFileCounter, 0) + if err := unmarshalObject(ctx.perfObjects["Paging File"], &pfc); err != nil { + return nil, err + } + + // Get current page file usage. + var pfbRaw float64 = 0 + for _, pageFile := range pfc { + if strings.Contains(strings.ToLower(pageFile.Name), "_total") { + continue + } + pfbRaw += pageFile.Usage + } + + // Subtract from total page file allocation on disk. + pfb := fsipf - (pfbRaw * float64(gpi.PageSize)) + ch <- prometheus.MustNewConstMetric( c.OSInformation, prometheus.GaugeValue, 1.0, - dst[0].Caption, - dst[0].Version, + fmt.Sprintf("Microsoft %s", pn), // Caption + fmt.Sprintf("%d.%d.%s", nwgi.VersionMajor, nwgi.VersionMinor, bn), // Version ) ch <- prometheus.MustNewConstMetric( c.PhysicalMemoryFreeBytes, prometheus.GaugeValue, - float64(dst[0].FreePhysicalMemory*1024), // KiB -> bytes + float64(gmse.AvailPhys), ) - time := dst[0].LocalDateTime - ch <- prometheus.MustNewConstMetric( c.Time, prometheus.GaugeValue, - float64(time.Unix()), + float64(currentTime.Unix()), ) - timezoneName, _ := time.Zone() - ch <- prometheus.MustNewConstMetric( c.Timezone, prometheus.GaugeValue, @@ -191,55 +263,58 @@ 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 + pfb, ) ch <- prometheus.MustNewConstMetric( c.VirtualMemoryFreeBytes, prometheus.GaugeValue, - float64(dst[0].FreeVirtualMemory*1024), // KiB -> bytes + float64(gmse.AvailPageFile), ) + // 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.TotalVirtual), ) 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.LoggedOnUsers), ) 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.TotalPageFile), ) ch <- prometheus.MustNewConstMetric( c.VisibleMemoryBytes, prometheus.GaugeValue, - float64(dst[0].TotalVisibleMemorySize*1024), // KiB -> bytes + float64(gmse.TotalPhys), ) return nil, nil diff --git a/collector/os_test.go b/collector/os_test.go new file mode 100644 index 00000000..1b6b7d9d --- /dev/null +++ b/collector/os_test.go @@ -0,0 +1,24 @@ +package collector + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" +) + +func BenchmarkOsCollect(b *testing.B) { + o, err := NewOSCollector() + if err != nil { + b.Error(err) + } + metrics := make(chan prometheus.Metric) + go func() { + for { + <-metrics + } + }() + s, err := PrepareScrapeContext([]string{"os"}) + for i := 0; i < b.N; i++ { + o.Collect(s, metrics) + } +} diff --git a/headers/netapi32/netapi32.go b/headers/netapi32/netapi32.go new file mode 100644 index 00000000..7b58029d --- /dev/null +++ b/headers/netapi32/netapi32.go @@ -0,0 +1,108 @@ +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 +} + +// WorkstationInfo is an idiomatic wrapper of WKSTAInfo102 +type WorkstationInfo struct { + PlatformId uint32 + ComputerName string + LanGroup string + VersionMajor uint32 + VersionMinor uint32 + LanRoot string + LoggedOnUsers 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) { + var lpwi *wKSTAInfo102 + pLevel := uintptr(102) + + r1, _, _ := procNetWkstaGetInfo.Call(0, pLevel, uintptr(unsafe.Pointer(&lpwi))) + defer netApiBufferFree(lpwi) + + if ret := *(*uint32)(unsafe.Pointer(&r1)); ret != 0 { + return wKSTAInfo102{}, ret, errors.New(NetApiStatus[ret]) + } + + deref := *lpwi + return deref, 0, nil +} + +// GetWorkstationInfo is an idiomatic wrapper for netWkstaGetInfo +func GetWorkstationInfo() (WorkstationInfo, error) { + info, _, err := netWkstaGetInfo() + if err != nil { + return WorkstationInfo{}, err + } + workstationInfo := WorkstationInfo{ + PlatformId: info.wki102_platform_id, + ComputerName: windows.UTF16PtrToString(info.wki102_computername), + LanGroup: windows.UTF16PtrToString(info.wki102_langroup), + VersionMajor: info.wki102_ver_major, + VersionMinor: info.wki102_ver_minor, + LanRoot: windows.UTF16PtrToString(info.wki102_lanroot), + LoggedOnUsers: info.wki102_logged_on_users, + } + return workstationInfo, nil +} diff --git a/headers/psapi/psapi.go b/headers/psapi/psapi.go new file mode 100644 index 00000000..77f837a9 --- /dev/null +++ b/headers/psapi/psapi.go @@ -0,0 +1,45 @@ +package psapi + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +// PerformanceInformation is a wrapper of the PERFORMANCE_INFORMATION struct. +// https://docs.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-performance_information +type PerformanceInformation struct { + cb uint32 + CommitTotal uint + CommitLimit uint + CommitPeak uint + PhysicalTotal uint + PhysicalAvailable uint + SystemCache uint + KernelTotal uint + KernelPaged uint + KernelNonpaged uint + PageSize uint + HandleCount uint32 + ProcessCount uint32 + ThreadCount uint32 +} + +var ( + psapi = windows.NewLazySystemDLL("psapi.dll") + procGetPerformanceInfo = psapi.NewProc("GetPerformanceInfo") +) + +// GetPerformanceInfo returns the dereferenced version of GetLPPerformanceInfo. +func GetPerformanceInfo() (PerformanceInformation, error) { + var lppi PerformanceInformation + size := (uint32)(unsafe.Sizeof(lppi)) + lppi.cb = size + r1, _, err := procGetPerformanceInfo.Call(uintptr(unsafe.Pointer(&lppi)), uintptr(size)) + + if ret := *(*bool)(unsafe.Pointer(&r1)); !ret { + return PerformanceInformation{}, err + } + + return lppi, nil +} diff --git a/headers/sysinfoapi/sysinfoapi.go b/headers/sysinfoapi/sysinfoapi.go new file mode 100644 index 00000000..41471bb9 --- /dev/null +++ b/headers/sysinfoapi/sysinfoapi.go @@ -0,0 +1,164 @@ +package sysinfoapi + +import ( + "unicode/utf16" + "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 +} + +// MemoryStatus is an idiomatic wrapper for MemoryStatusEx +type MemoryStatus struct { + MemoryLoad uint32 + TotalPhys uint64 + AvailPhys uint64 + TotalPageFile uint64 + AvailPageFile uint64 + TotalVirtual uint64 + AvailVirtual uint64 + AvailExtendedVirtual uint64 +} + +// wProcessorArchitecture is a wrapper for the union found in LP_SYSTEM_INFO +// https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/ns-sysinfoapi-system_info +type wProcessorArchitecture struct { + WReserved uint16 + WProcessorArchitecture uint16 +} + +// ProcessorArchitecture is an idiomatic wrapper for wProcessorArchitecture +type ProcessorArchitecture uint16 + +// Idiomatic values for wProcessorArchitecture +const ( + AMD64 ProcessorArchitecture = 9 + ARM = 5 + ARM64 = 12 + IA64 = 6 + INTEL = 0 + UNKNOWN = 0xffff +) + +// LpSystemInfo is a wrapper for LPSYSTEM_INFO +// https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/ns-sysinfoapi-system_info +type lpSystemInfo struct { + Arch wProcessorArchitecture + DwPageSize uint32 + LpMinimumApplicationAddress uintptr + LpMaximumApplicationAddress uintptr + DwActiveProcessorMask uint32 + DwNumberOfProcessors uint32 + DwProcessorType uint32 + DwAllocationGranularity uint32 + WProcessorLevel uint16 + WProcessorRevision uint16 +} + +// SystemInfo is an idiomatic wrapper for LpSystemInfo +type SystemInfo struct { + Arch ProcessorArchitecture + PageSize uint32 + MinimumApplicationAddress uintptr + MaximumApplicationAddress uintptr + ActiveProcessorMask uint32 + NumberOfProcessors uint32 + ProcessorType uint32 + AllocationGranularity uint32 + ProcessorLevel uint16 + ProcessorRevision uint16 +} + +// WinComputerNameFormat is a wrapper for COMPUTER_NAME_FORMAT +type WinComputerNameFormat int + +// Definitions for WinComputerNameFormat constants +const ( + ComputerNameNetBIOS WinComputerNameFormat = iota + ComputerNameDNSHostname + ComputerNameDNSDomain + ComputerNameDNSFullyQualified + ComputerNamePhysicalNetBIOS + ComputerNamePhysicalDNSHostname + ComputerNamePhysicalDNSDomain + ComputerNamePhysicalDNSFullyQualified + ComputerNameMax +) + +var ( + kernel32 = windows.NewLazySystemDLL("kernel32.dll") + procGetSystemInfo = kernel32.NewProc("GetSystemInfo") + procGlobalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx") + procGetComputerNameExW = kernel32.NewProc("GetComputerNameExW") +) + +// 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() (MemoryStatus, error) { + var mse memoryStatusEx + mse.dwLength = (uint32)(unsafe.Sizeof(mse)) + r1, _, err := procGlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&mse))) + + if ret := *(*bool)(unsafe.Pointer(&r1)); ret == false { + return MemoryStatus{}, err + } + + return MemoryStatus{ + MemoryLoad: mse.DwMemoryLoad, + TotalPhys: mse.UllTotalPhys, + AvailPhys: mse.UllAvailPhys, + TotalPageFile: mse.UllTotalPageFile, + AvailPageFile: mse.UllAvailPageFile, + TotalVirtual: mse.UllTotalVirtual, + AvailVirtual: mse.UllAvailVirtual, + AvailExtendedVirtual: mse.UllAvailExtendedVirtual, + }, nil +} + +// GetSystemInfo is an idiomatic wrapper for the GetSystemInfo function from sysinfoapi +// https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsysteminfo +func GetSystemInfo() SystemInfo { + var info lpSystemInfo + procGetSystemInfo.Call(uintptr(unsafe.Pointer(&info))) + return SystemInfo{ + Arch: ProcessorArchitecture(info.Arch.WProcessorArchitecture), + PageSize: info.DwPageSize, + MinimumApplicationAddress: info.LpMinimumApplicationAddress, + MaximumApplicationAddress: info.LpMinimumApplicationAddress, + ActiveProcessorMask: info.DwActiveProcessorMask, + NumberOfProcessors: info.DwNumberOfProcessors, + ProcessorType: info.DwProcessorType, + AllocationGranularity: info.DwAllocationGranularity, + ProcessorLevel: info.WProcessorLevel, + ProcessorRevision: info.WProcessorRevision, + } +} + +// GetComputerName wraps the GetComputerNameW function in a more Go-like way +// https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getcomputernameexw +func GetComputerName(f WinComputerNameFormat) (string, error) { + // 1kb buffer to accept computer name. This should be more than enough as the maximum size + // returned is the max length of a DNS name, which this author believes is 253 characters. + size := 1024 + var buffer [1024]uint16 + r1, _, err := procGetComputerNameExW.Call(uintptr(f), uintptr(unsafe.Pointer(&buffer)), uintptr(unsafe.Pointer(&size))) + if r1 == 0 { + return "", err + } + bytes := buffer[0:size] + out := utf16.Decode(bytes) + return string(out), nil +}