process: read processes from the kernel instead of perflib (#2644)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2026-10-10 16:09:02 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 4ca7dc8957
commit 651daf0fee
24 changed files with 1078 additions and 2651 deletions
+28 -1
View File
@@ -1,4 +1,31 @@
# process collector
# Process collector
The collector reads all processes with one
`NtQuerySystemInformation(SystemProcessInformation)` call, not with the perflib
`Process` or PDH `Process V2` counter sets. The perflib provider (perfproc) reads
the same kernel data, so the metric values must stay identical to the `Process`
counter set. There is no counter-set selection; `--collector.process.counter-version`
and `Config.CounterVersion` were removed.
- Process names follow the perflib instance names: the image name without a
case-insensitive `.exe` suffix, other extensions such as `.scr` are kept, and
PID 0 is `Idle`. Users' include and exclude expressions depend on these names.
- The `windows_process_info` labels (owner, command line, process group ID) need
`OpenProcess` and are cached by PID and creation time, because Windows reuses
PIDs. The creation time of the opened handle is compared with the snapshot,
so a PID reused between the snapshot and the lookup is not attributed to the
old process. Failed lookups are not cached and retry on the next scrape;
access-denied results are cached as empty labels.
- Read the snapshot buffer with bounds checks against the returned length, and
keep it 8-byte aligned (`[]uint64` backing array).
To check value parity after changes, compare the snapshot against the `Process`
counter set by PID in a temporary test, for example with
`pdh.NewCollector(..., pdh.CounterTypeRaw, "Process", pdh.InstancesAll)`, including
processes with names like `a.b.exe` and `tool.scr`. Run it as a standard user
too: neither data source needs administrator rights.
## IIS application pools
IIS application pool names (`--collector.process.iis`) come from two sources.
+2 -2
View File
@@ -27,13 +27,13 @@ import (
"golang.org/x/sys/windows"
)
// workerProcessName is the perflib instance name of IIS worker processes (w3wp.exe).
// workerProcessName is the process name of IIS worker processes (w3wp.exe).
const workerProcessName = "w3wp"
// errNoAppPoolArgument reports a w3wp command line without a non-empty -ap argument.
var errNoAppPoolArgument = errors.New("command line has no -ap argument")
// isWorkerProcess reports whether the perflib process name (without instance suffix) is an IIS worker process.
// isWorkerProcess reports whether the process name is an IIS worker process.
func isWorkerProcess(name string) bool {
return strings.EqualFold(name, workerProcessName)
}
@@ -370,7 +370,6 @@ func TestBuildCloseWorkerProcess(t *testing.T) {
ProcessInclude: regexp.MustCompile(`^(?:w3wp)$`),
ProcessExclude: types.RegExpEmpty,
EnableWorkerProcess: true,
CounterVersion: 1,
})
logger := slog.New(slog.DiscardHandler)
+270 -198
View File
@@ -25,16 +25,11 @@ import (
"regexp"
"slices"
"strconv"
"strings"
"sync"
"time"
"unsafe"
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"github.com/prometheus-community/windows_exporter/internal/pdh/registry"
pdhtypes "github.com/prometheus-community/windows_exporter/internal/pdh/types"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/sys/windows"
@@ -42,12 +37,21 @@ import (
const Name = "process"
const (
// windowsEpoch is the FILETIME of the Unix epoch, in 100ns intervals since 1601.
windowsEpoch int64 = 116444736000000000
// ticksPerSecond is the number of 100ns intervals per second.
ticksPerSecond = 1e7
// secondsPerTick converts 100ns intervals to seconds. Multiplying by it, like the perflib
// reader did, keeps the CPU time values bit-identical to the Process counter set.
secondsPerTick = 1 / ticksPerSecond
)
type Config struct {
ProcessInclude *regexp.Regexp `yaml:"include"`
ProcessExclude *regexp.Regexp `yaml:"exclude"`
EnableWorkerProcess bool `yaml:"iis"`
EnableCMDLine bool `yaml:"cmdline"`
CounterVersion uint8 `yaml:"counter-version"`
}
//nolint:gochecknoglobals
@@ -56,7 +60,6 @@ var ConfigDefaults = Config{
ProcessExclude: types.RegExpEmpty,
EnableWorkerProcess: false,
EnableCMDLine: true,
CounterVersion: 1,
}
type Collector struct {
@@ -69,10 +72,10 @@ type Collector struct {
miSession *mi.Session
workerProcessMIQueryQuery mi.Query
perfDataCollector pdhtypes.Collector[perfDataCounterValues]
perfDataObject []perfDataCounterValues
workerCh chan processWorkerRequest
workerWG sync.WaitGroup
snapshot snapshotBuffer
processes []processSnapshot
// infoCache holds the windows_process_info values of the processes of the last scrape, keyed by PID.
infoCache map[uint32]processInfo
lookupCache sync.Map
@@ -143,11 +146,6 @@ func NewWithFlags(app *kingpin.Application) *Collector {
"If enabled, the full cmdline is exposed to the windows_process_info metrics.",
).Default(strconv.FormatBool(c.config.EnableCMDLine)).BoolVar(&c.config.EnableCMDLine)
app.Flag(
"collector.process.counter-version",
"Version of the process collector to use. 1 for Process V1, 2 for Process V2, 0 for Process V2 with fallback to Process V1 if V2 is not available. Defaults to 1.",
).Default(strconv.FormatUint(uint64(c.config.CounterVersion), 10)).Uint8Var(&c.config.CounterVersion)
app.Action(func(*kingpin.ParseContext) error {
var err error
@@ -175,15 +173,9 @@ func (c *Collector) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.perfDataCollector != nil {
c.perfDataCollector.Close()
}
if c.workerCh != nil {
close(c.workerCh)
c.workerCh = nil
c.workerWG.Wait()
}
c.snapshot = snapshotBuffer{}
c.processes = nil
c.infoCache = nil
// The session belongs to the caller of Build.
c.miSession = nil
@@ -194,40 +186,7 @@ func (c *Collector) Close() error {
func (c *Collector) Build(logger *slog.Logger, miSession *mi.Session) error {
c.logger = logger.With(slog.String("collector", Name))
var err error
switch c.config.CounterVersion {
case 2:
c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](c.logger, pdh.CounterTypeRaw, "Process V2", pdh.InstancesAll)
case 1:
c.perfDataCollector, err = registry.NewCollector[perfDataCounterValues]("Process", pdh.InstancesAll)
default:
c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](c.logger, pdh.CounterTypeRaw, "Process V2", pdh.InstancesAll)
c.config.CounterVersion = 2
if errors.Is(err, pdh.NewPdhError(pdh.CstatusNoObject)) {
// NewCollector returns a partially built collector on errors. Release it before falling back to V1.
c.perfDataCollector.Close()
c.perfDataCollector, err = registry.NewCollector[perfDataCounterValues]("Process", pdh.InstancesAll)
c.config.CounterVersion = 1
}
c.logger.LogAttrs(context.Background(), slog.LevelDebug, fmt.Sprintf("Using process collector V%d", c.config.CounterVersion))
}
if err != nil {
return fmt.Errorf("failed to create Process V%d collector: %w", c.config.CounterVersion, err)
}
c.workerCh = make(chan processWorkerRequest, 32)
for range 4 {
c.workerWG.Add(1)
go c.collectWorker(c.workerCh)
}
c.mu = sync.RWMutex{}
c.infoCache = nil
c.lookupCache = sync.Map{}
// The flags wrap the expressions in ^(?:...)$, the defaults of [New] are [types.RegExpAny] and [types.RegExpEmpty].
@@ -379,173 +338,286 @@ func (c *Collector) buildWorkerProcessWMI(miSession *mi.Session) error {
}
func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error {
return c.collect(ch, maxScrapeDuration)
c.mu.Lock()
defer c.mu.Unlock()
var err error
c.processes, err = c.snapshot.query(c.processes)
if err != nil {
return fmt.Errorf("failed to query processes: %w", err)
}
processes := make([]*processSnapshot, 0, len(c.processes))
var workerProcessIDs []uint32
for i := range c.processes {
process := &c.processes[i]
if c.config.ProcessExclude.MatchString(process.name) || !c.config.ProcessInclude.MatchString(process.name) {
continue
}
if c.config.EnableWorkerProcess && isWorkerProcess(process.name) {
workerProcessIDs = append(workerProcessIDs, process.pid)
}
processes = append(processes, process)
}
var appPools map[uint32]string
if len(workerProcessIDs) > 0 {
appPools, err = resolveAppPools(c.logger, workerProcessIDs, workerProcessAppPool, c.workerProcessWMIQuery(maxScrapeDuration))
}
infos := c.resolveProcessInfo(processes)
for i, process := range processes {
name := process.name
if appPoolName := appPools[process.pid]; appPoolName != "" {
name = name + "_" + appPoolName
}
c.collectProcess(ch, name, process, &infos[i])
}
return err
}
// ref: https://github.com/microsoft/hcsshim/blob/8beabacfc2d21767a07c20f8dd5f9f3932dbf305/internal/uvm/stats.go#L25
func (c *Collector) getProcessInformation(pid uint32) (string, string, uint32, error) {
if pid == 0 {
return "", "", 0, nil
// workerProcessWMIQuery returns the root\WebAdministration query, or nil if it was unavailable during Build.
func (c *Collector) workerProcessWMIQuery(maxScrapeDuration time.Duration) func() ([]WorkerProcess, error) {
miSession, miQuery := c.miSession, c.workerProcessMIQueryQuery
if miSession == nil {
return nil
}
hProcess, vmReadAccess, err := c.openProcess(pid)
if err != nil {
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
return "", "", 0, nil
}
return func() ([]WorkerProcess, error) {
var workerProcesses []WorkerProcess
return "", "", 0, err
err := miSession.Query(&workerProcesses, mi.NamespaceRootWebAdministration, miQuery, maxScrapeDuration)
return workerProcesses, err
}
defer func(hProcess windows.Handle) {
if err := windows.CloseHandle(hProcess); err != nil {
c.logger.Warn("CloseHandle failed",
slog.Any("err", err),
)
}
}(hProcess)
owner, err := c.getProcessOwner(c.logger, hProcess)
if err != nil {
return "", "", 0, err
}
var (
cmdLine string
processGroupID uint32
)
if vmReadAccess {
cmdLine, processGroupID, err = c.getExtendedProcessInformation(hProcess)
if err != nil {
return "", owner, processGroupID, err
}
}
return cmdLine, owner, processGroupID, nil
}
func (c *Collector) getExtendedProcessInformation(hProcess windows.Handle) (string, uint32, error) {
// Get the process environment block (PEB) address
var pbi windows.PROCESS_BASIC_INFORMATION
// resolveAppPools maps IIS worker process IDs to application pool names.
//
// The command line of the worker process is the primary source: it needs no
// optional feature and no WMI round trip. WMI is queried at most once, for the
// worker processes whose command line couldn't be read or has no -ap argument,
// and only if queryWMI isn't nil. Unresolved processes have no entry.
func resolveAppPools(
logger *slog.Logger,
pids []uint32,
readCommandLine func(pid uint32) (string, error),
queryWMI func() ([]WorkerProcess, error),
) (map[uint32]string, error) {
appPools := make(map[uint32]string, len(pids))
retLen := uint32(unsafe.Sizeof(pbi))
if err := windows.NtQueryInformationProcess(hProcess, windows.ProcessBasicInformation, unsafe.Pointer(&pbi), retLen, &retLen); err != nil {
return "", 0, fmt.Errorf("failed to query process basic information: %w", err)
}
var unresolved []uint32
peb := windows.PEB{}
for _, pid := range pids {
appPool, err := readCommandLine(pid)
if err == nil {
appPools[pid] = appPool
err := windows.ReadProcessMemory(hProcess,
uintptr(unsafe.Pointer(pbi.PebBaseAddress)),
(*byte)(unsafe.Pointer(&peb)),
unsafe.Sizeof(peb),
nil,
)
if err != nil {
return "", 0, fmt.Errorf("failed to read process memory: %w", err)
}
continue
}
processParameters := windows.RTL_USER_PROCESS_PARAMETERS{}
err = windows.ReadProcessMemory(hProcess,
uintptr(unsafe.Pointer(peb.ProcessParameters)),
(*byte)(unsafe.Pointer(&processParameters)),
unsafe.Sizeof(processParameters),
nil,
)
if err != nil {
return "", 0, fmt.Errorf("failed to read process memory: %w", err)
}
var cmdLine string
if c.config.EnableCMDLine {
cmdLineUTF16 := make([]uint16, processParameters.CommandLine.Length)
err = windows.ReadProcessMemory(hProcess,
uintptr(unsafe.Pointer(processParameters.CommandLine.Buffer)),
(*byte)(unsafe.Pointer(&cmdLineUTF16[0])),
uintptr(processParameters.CommandLine.Length),
nil,
logger.LogAttrs(context.Background(), slog.LevelDebug, "Failed to read the IIS application pool from the worker process command line",
slog.Uint64("pid", uint64(pid)),
slog.Any("err", err),
)
if err != nil {
return "", processParameters.ProcessGroupId, fmt.Errorf("failed to read process memory: %w", err)
}
cmdLine = strings.TrimSpace(windows.UTF16ToString(cmdLineUTF16))
// OpenProcess fails with ERROR_INVALID_PARAMETER for processes that exited after the
// process snapshot. WAS no longer reports them either.
if !errors.Is(err, windows.ERROR_INVALID_PARAMETER) {
unresolved = append(unresolved, pid)
}
}
return cmdLine, processParameters.ProcessGroupId, nil
}
func (c *Collector) getProcessOwner(logger *slog.Logger, hProcess windows.Handle) (string, error) {
var tok windows.Token
if err := windows.OpenProcessToken(hProcess, windows.TOKEN_QUERY, &tok); err != nil {
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
return "", nil
}
return "", fmt.Errorf("failed to open process token: %w", err)
if len(unresolved) == 0 || queryWMI == nil {
return appPools, nil
}
defer func(tok windows.Token) {
if err := tok.Close(); err != nil {
logger.Warn("Token close failed",
slog.Any("err", err),
)
}
}(tok)
tokenUser, err := tok.GetTokenUser()
workerProcesses, err := queryWMI()
if err != nil {
return "", fmt.Errorf("failed to get token user: %w", err)
return appPools, fmt.Errorf("WMI query for collector.process.iis failed: %w", err)
}
sid := tokenUser.User.Sid.String()
var owner string
ownerVal, ok := c.lookupCache.Load(sid)
if ok {
owner, ok = ownerVal.(string)
}
if !ok {
account, domain, _, err := tokenUser.User.Sid.LookupAccount("")
if err != nil {
owner = sid
} else {
owner = fmt.Sprintf(`%s\%s`, domain, account)
byPID := make(map[uint64]string, len(workerProcesses))
for _, wp := range workerProcesses {
if _, ok := byPID[wp.ProcessId]; !ok && wp.AppPoolName != "" {
byPID[wp.ProcessId] = wp.AppPoolName
}
c.lookupCache.Store(sid, owner)
}
return owner, nil
for _, pid := range unresolved {
if appPool, ok := byPID[uint64(pid)]; ok {
appPools[pid] = appPool
}
}
return appPools, nil
}
func (c *Collector) openProcess(pid uint32) (windows.Handle, bool, error) {
// Open the process with QUERY_INFORMATION and VM_READ permissions.
hProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, pid)
if err == nil {
return hProcess, true, nil
}
func (c *Collector) collectProcess(ch chan<- prometheus.Metric, name string, process *processSnapshot, info *processInfo) {
pid := strconv.FormatUint(uint64(process.pid), 10)
if !errors.Is(err, windows.ERROR_ACCESS_DENIED) {
return 0, false, fmt.Errorf("failed to open process: %w", err)
}
ch <- prometheus.MustNewConstMetric(
c.info,
prometheus.GaugeValue,
1.0,
name, pid, strconv.FormatUint(uint64(process.parentPID), 10), strconv.FormatUint(uint64(info.processGroupID), 10), info.owner, info.cmdLine,
)
if errors.Is(err, windows.Errno(0x57)) { // invalid parameter, for PIDs that don't exist
return 0, false, errors.New("process not found")
}
// Perflib reports whole seconds for the Elapsed Time counter of the Process counter set.
ch <- prometheus.MustNewConstMetric(
c.startTime,
prometheus.GaugeValue,
float64((process.createTime-windowsEpoch)/ticksPerSecond),
name, pid,
)
hProcess, err = windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
if err != nil {
return 0, false, fmt.Errorf("failed to open process with limited permissions: %w", err)
}
ch <- prometheus.MustNewConstMetric(
c.handleCount,
prometheus.GaugeValue,
float64(process.handleCount),
name, pid,
)
return hProcess, false, nil
ch <- prometheus.MustNewConstMetric(
c.cpuTimeTotal,
prometheus.CounterValue,
float64(process.kernelTime)*secondsPerTick,
name, pid, "privileged",
)
ch <- prometheus.MustNewConstMetric(
c.cpuTimeTotal,
prometheus.CounterValue,
float64(process.userTime)*secondsPerTick,
name, pid, "user",
)
ch <- prometheus.MustNewConstMetric(
c.ioBytesTotal,
prometheus.CounterValue,
float64(process.otherTransferCount),
name, pid, "other",
)
ch <- prometheus.MustNewConstMetric(
c.ioOperationsTotal,
prometheus.CounterValue,
float64(process.otherOperationCount),
name, pid, "other",
)
ch <- prometheus.MustNewConstMetric(
c.ioBytesTotal,
prometheus.CounterValue,
float64(process.readTransferCount),
name, pid, "read",
)
ch <- prometheus.MustNewConstMetric(
c.ioOperationsTotal,
prometheus.CounterValue,
float64(process.readOperationCount),
name, pid, "read",
)
ch <- prometheus.MustNewConstMetric(
c.ioBytesTotal,
prometheus.CounterValue,
float64(process.writeTransferCount),
name, pid, "write",
)
ch <- prometheus.MustNewConstMetric(
c.ioOperationsTotal,
prometheus.CounterValue,
float64(process.writeOperationCount),
name, pid, "write",
)
ch <- prometheus.MustNewConstMetric(
c.pageFaultsTotal,
prometheus.CounterValue,
float64(process.pageFaultCount),
name, pid,
)
ch <- prometheus.MustNewConstMetric(
c.pageFileBytes,
prometheus.GaugeValue,
float64(process.pageFileUsage),
name, pid,
)
ch <- prometheus.MustNewConstMetric(
c.poolBytes,
prometheus.GaugeValue,
float64(process.nonPagedPoolUsage),
name, pid, "nonpaged",
)
ch <- prometheus.MustNewConstMetric(
c.poolBytes,
prometheus.GaugeValue,
float64(process.pagedPoolUsage),
name, pid, "paged",
)
ch <- prometheus.MustNewConstMetric(
c.priorityBase,
prometheus.GaugeValue,
float64(process.basePriority),
name, pid,
)
ch <- prometheus.MustNewConstMetric(
c.privateBytes,
prometheus.GaugeValue,
float64(process.privatePageCount),
name, pid,
)
ch <- prometheus.MustNewConstMetric(
c.threadCount,
prometheus.GaugeValue,
float64(process.threadCount),
name, pid,
)
ch <- prometheus.MustNewConstMetric(
c.virtualBytes,
prometheus.GaugeValue,
float64(process.virtualSize),
name, pid,
)
ch <- prometheus.MustNewConstMetric(
c.workingSetPrivate,
prometheus.GaugeValue,
float64(process.workingSetPrivate),
name, pid,
)
ch <- prometheus.MustNewConstMetric(
c.workingSetPeak,
prometheus.GaugeValue,
float64(process.workingSetPeak),
name, pid,
)
ch <- prometheus.MustNewConstMetric(
c.workingSet,
prometheus.GaugeValue,
float64(process.workingSetSize),
name, pid,
)
}
@@ -93,7 +93,6 @@ func TestCollectorIISAppPool(t *testing.T) {
ProcessInclude: regexp.MustCompile(`^(?:w3wp)$`),
ProcessExclude: types.RegExpEmpty,
EnableWorkerProcess: true,
CounterVersion: 1,
})
require.Contains(t, families, "windows_process_info")
@@ -142,12 +141,10 @@ func TestCollectorWorkerProcessCommandLine(t *testing.T) {
ProcessInclude: regexp.MustCompile(`^(?:w3wp)$`),
ProcessExclude: types.RegExpEmpty,
EnableWorkerProcess: true,
CounterVersion: 1,
})
if _, ok := families["windows_process_info"]; !ok {
t.Skip("the Process counter set has no w3wp instance")
}
// The process snapshot includes suspended processes.
require.Contains(t, families, "windows_process_info", "no w3wp process in the process snapshot")
for _, metric := range families["windows_process_info"].GetMetric() {
labels := make(map[string]string)
+328
View File
@@ -0,0 +1,328 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package process
import (
"context"
"errors"
"fmt"
"log/slog"
"runtime"
"runtime/debug"
"strings"
"sync"
"sync/atomic"
"unsafe"
"golang.org/x/sys/windows"
)
// maxInfoWorkers bounds the goroutines that open processes for new cache entries.
const maxInfoWorkers = 8
// errProcessGone reports that the PID no longer belongs to the snapshot process.
var errProcessGone = errors.New("process exited")
// processInfo holds the values of windows_process_info that are fixed for the lifetime of a process.
type processInfo struct {
// createTime identifies the process together with the PID, because Windows reuses PIDs.
createTime int64
cmdLine string
owner string
processGroupID uint32
}
// resolveProcessInfo returns the info of each process, in the order of processes.
// Cached entries are reused while the PID and creation time match. The others are
// looked up in parallel. The cache keeps only the processes passed in.
func (c *Collector) resolveProcessInfo(processes []*processSnapshot) []processInfo {
infos := make([]processInfo, len(processes))
cached := make([]bool, len(processes))
misses := make([]int, 0)
for i, process := range processes {
if info, ok := c.infoCache[process.pid]; ok && info.createTime == process.createTime {
infos[i] = info
cached[i] = true
continue
}
misses = append(misses, i)
}
if len(misses) > 0 {
var next atomic.Int64
wg := sync.WaitGroup{}
for range min(len(misses), runtime.GOMAXPROCS(0), maxInfoWorkers) {
wg.Go(func() {
for {
n := int(next.Add(1)) - 1
if n >= len(misses) {
return
}
i := misses[n]
c.lookupProcessInfo(processes[i], &infos[i], &cached[i])
}
})
}
wg.Wait()
}
cache := make(map[uint32]processInfo, len(processes))
for i, process := range processes {
if cached[i] {
cache[process.pid] = infos[i]
}
}
c.infoCache = cache
return infos
}
// lookupProcessInfo opens the process and reads its owner, command line and process group ID.
// cacheable is set only if the lookup succeeded. A panic leaves both values unset.
func (c *Collector) lookupProcessInfo(process *processSnapshot, info *processInfo, cacheable *bool) {
defer func() {
if r := recover(); r != nil {
c.logger.LogAttrs(context.Background(), slog.LevelError, "Panic while reading process information",
slog.Uint64("pid", uint64(process.pid)),
slog.Any("panic", r),
slog.String("stack", string(debug.Stack())),
)
}
}()
cmdLine, owner, processGroupID, err := c.getProcessInformation(process.pid, process.createTime)
if err != nil {
if !errors.Is(err, errProcessGone) {
c.logger.LogAttrs(context.Background(), slog.LevelDebug, "Failed to get process information",
slog.Uint64("pid", uint64(process.pid)),
slog.String("process", process.name),
slog.Any("err", err),
)
}
}
*info = processInfo{
createTime: process.createTime,
cmdLine: cmdLine,
owner: owner,
processGroupID: processGroupID,
}
*cacheable = err == nil
}
// getProcessInformation returns the command line, owner and process group ID of a process.
// createTime is checked after opening the process, so a reused PID is not attributed to the old process.
//
// ref: https://github.com/microsoft/hcsshim/blob/8beabacfc2d21767a07c20f8dd5f9f3932dbf305/internal/uvm/stats.go#L25
func (c *Collector) getProcessInformation(pid uint32, createTime int64) (string, string, uint32, error) {
if pid == 0 {
return "", "", 0, nil
}
hProcess, vmReadAccess, err := c.openProcess(pid)
if err != nil {
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
return "", "", 0, nil
}
return "", "", 0, err
}
defer func(hProcess windows.Handle) {
if err := windows.CloseHandle(hProcess); err != nil {
c.logger.Warn("CloseHandle failed",
slog.Any("err", err),
)
}
}(hProcess)
var creationTime, exitTime, kernelTime, userTime windows.Filetime
if err := windows.GetProcessTimes(hProcess, &creationTime, &exitTime, &kernelTime, &userTime); err == nil &&
createTime != 0 && int64(creationTime.HighDateTime)<<32|int64(creationTime.LowDateTime) != createTime {
return "", "", 0, errProcessGone
}
owner, err := c.getProcessOwner(c.logger, hProcess)
if err != nil {
return "", "", 0, err
}
var (
cmdLine string
processGroupID uint32
)
if vmReadAccess {
cmdLine, processGroupID, err = c.getExtendedProcessInformation(hProcess)
if err != nil {
return "", owner, processGroupID, err
}
}
return cmdLine, owner, processGroupID, nil
}
func (c *Collector) getExtendedProcessInformation(hProcess windows.Handle) (string, uint32, error) {
// Get the process environment block (PEB) address
var pbi windows.PROCESS_BASIC_INFORMATION
retLen := uint32(unsafe.Sizeof(pbi))
if err := windows.NtQueryInformationProcess(hProcess, windows.ProcessBasicInformation, unsafe.Pointer(&pbi), retLen, &retLen); err != nil {
return "", 0, fmt.Errorf("failed to query process basic information: %w", err)
}
// Minimal processes like Registry and Memory Compression have no PEB.
if pbi.PebBaseAddress == nil {
return "", 0, nil
}
peb := windows.PEB{}
err := windows.ReadProcessMemory(hProcess,
uintptr(unsafe.Pointer(pbi.PebBaseAddress)),
(*byte)(unsafe.Pointer(&peb)),
unsafe.Sizeof(peb),
nil,
)
if err != nil {
return "", 0, fmt.Errorf("failed to read process memory: %w", err)
}
if peb.ProcessParameters == nil {
return "", 0, nil
}
processParameters := windows.RTL_USER_PROCESS_PARAMETERS{}
err = windows.ReadProcessMemory(hProcess,
uintptr(unsafe.Pointer(peb.ProcessParameters)),
(*byte)(unsafe.Pointer(&processParameters)),
unsafe.Sizeof(processParameters),
nil,
)
if err != nil {
return "", 0, fmt.Errorf("failed to read process memory: %w", err)
}
var cmdLine string
// CommandLine.Length is in bytes.
if c.config.EnableCMDLine && processParameters.CommandLine.Length >= 2 && processParameters.CommandLine.Buffer != nil {
cmdLineUTF16 := make([]uint16, processParameters.CommandLine.Length/2)
err = windows.ReadProcessMemory(hProcess,
uintptr(unsafe.Pointer(processParameters.CommandLine.Buffer)),
(*byte)(unsafe.Pointer(&cmdLineUTF16[0])),
uintptr(len(cmdLineUTF16)*2),
nil,
)
if err != nil {
return "", processParameters.ProcessGroupId, fmt.Errorf("failed to read process memory: %w", err)
}
cmdLine = strings.TrimSpace(windows.UTF16ToString(cmdLineUTF16))
}
return cmdLine, processParameters.ProcessGroupId, nil
}
func (c *Collector) getProcessOwner(logger *slog.Logger, hProcess windows.Handle) (string, error) {
var tok windows.Token
if err := windows.OpenProcessToken(hProcess, windows.TOKEN_QUERY, &tok); err != nil {
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
return "", nil
}
return "", fmt.Errorf("failed to open process token: %w", err)
}
defer func(tok windows.Token) {
if err := tok.Close(); err != nil {
logger.Warn("Token close failed",
slog.Any("err", err),
)
}
}(tok)
tokenUser, err := tok.GetTokenUser()
if err != nil {
return "", fmt.Errorf("failed to get token user: %w", err)
}
sid := tokenUser.User.Sid.String()
var owner string
ownerVal, ok := c.lookupCache.Load(sid)
if ok {
owner, ok = ownerVal.(string)
}
if !ok {
account, domain, _, err := tokenUser.User.Sid.LookupAccount("")
if err != nil {
owner = sid
} else {
owner = fmt.Sprintf(`%s\%s`, domain, account)
}
c.lookupCache.Store(sid, owner)
}
return owner, nil
}
func (c *Collector) openProcess(pid uint32) (windows.Handle, bool, error) {
// Open the process with QUERY_INFORMATION and VM_READ permissions.
hProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, pid)
if err == nil {
return hProcess, true, nil
}
if errors.Is(err, windows.ERROR_INVALID_PARAMETER) { // The PID does not exist anymore.
return 0, false, errProcessGone
}
if !errors.Is(err, windows.ERROR_ACCESS_DENIED) {
return 0, false, fmt.Errorf("failed to open process: %w", err)
}
hProcess, err = windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
if err != nil {
if errors.Is(err, windows.ERROR_INVALID_PARAMETER) {
return 0, false, errProcessGone
}
return 0, false, fmt.Errorf("failed to open process with limited permissions: %w", err)
}
return hProcess, false, nil
}
@@ -18,11 +18,15 @@
package process_test
import (
"os"
"regexp"
"strconv"
"testing"
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/collector/process"
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
"github.com/stretchr/testify/require"
)
func BenchmarkProcessCollector(b *testing.B) {
@@ -37,3 +41,40 @@ func BenchmarkProcessCollector(b *testing.B) {
func TestCollector(t *testing.T) {
testutils.TestCollector(t, process.New, nil)
}
func TestCollectorCurrentProcess(t *testing.T) {
config := process.ConfigDefaults
config.ProcessInclude = regexp.MustCompile(`^process\.test$`)
families := testutils.TestCollector(t, process.New, &config)
pid := strconv.Itoa(os.Getpid())
infos := families["windows_process_info"].GetMetric()
require.Len(t, infos, 1)
labels := map[string]string{}
for _, label := range infos[0].GetLabel() {
labels[label.GetName()] = label.GetValue()
}
require.Equal(t, "process.test", labels["process"])
require.Equal(t, pid, labels["process_id"])
require.Equal(t, strconv.Itoa(os.Getppid()), labels["creating_process_id"])
require.NotEmpty(t, labels["owner"])
require.Contains(t, labels["cmdline"], "process.test")
for _, name := range []string{
"windows_process_start_time_seconds_timestamp",
"windows_process_handles",
"windows_process_threads",
"windows_process_virtual_bytes",
"windows_process_working_set_bytes",
"windows_process_private_bytes",
} {
require.Positive(t, testutils.MetricValuesByLabel(families, name, "process_id")[pid], name)
}
cpu := families["windows_process_cpu_time_total"].GetMetric()
require.Len(t, cpu, 2)
require.NotNil(t, cpu[0].GetCounter())
}
@@ -1,380 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package process
import (
"context"
"errors"
"fmt"
"log/slog"
"runtime/debug"
"strconv"
"strings"
"sync"
"time"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/sys/windows"
)
type processWorkerRequest struct {
ch chan<- prometheus.Metric
name string
appPoolName string
performanceCounterValues perfDataCounterValues
waitGroup *sync.WaitGroup
}
func (c *Collector) collect(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error {
err := c.perfDataCollector.Collect(&c.perfDataObject)
if err != nil {
return fmt.Errorf("failed to collect metrics: %w", err)
}
requests := make([]processWorkerRequest, 0, len(c.perfDataObject))
var workerProcessIDs []uint32
for _, process := range c.perfDataObject {
// Duplicate processes are suffixed #, and an index number. Remove those.
name, _, _ := strings.Cut(process.Name, ":") // Process V2
// Duplicate processes are suffixed #, and an index number. Remove those.
name, _, _ = strings.Cut(name, "#") // Process V1
if c.config.ProcessExclude.MatchString(name) || !c.config.ProcessInclude.MatchString(name) {
continue
}
if process.ProcessID == 0 && name != "Idle" {
c.logger.LogAttrs(context.Background(), slog.LevelDebug, "Skipping process with PID 0",
slog.String("name", name),
slog.String("process_name", process.Name),
slog.Any("process", fmt.Sprintf("%+v", process)),
)
continue
}
if c.config.EnableWorkerProcess && isWorkerProcess(name) {
workerProcessIDs = append(workerProcessIDs, uint32(process.ProcessID))
}
requests = append(requests, processWorkerRequest{
ch: ch,
name: name,
performanceCounterValues: process,
})
}
var appPools map[uint32]string
if len(workerProcessIDs) > 0 {
appPools, err = resolveAppPools(c.logger, workerProcessIDs, workerProcessAppPool, c.workerProcessWMIQuery(maxScrapeDuration))
}
wg := &sync.WaitGroup{}
for _, req := range requests {
req.appPoolName = appPools[uint32(req.performanceCounterValues.ProcessID)]
req.waitGroup = wg
wg.Add(1)
c.workerCh <- req
}
wg.Wait()
return err
}
// workerProcessWMIQuery returns the root\WebAdministration query, or nil if it was unavailable during Build.
func (c *Collector) workerProcessWMIQuery(maxScrapeDuration time.Duration) func() ([]WorkerProcess, error) {
miSession, miQuery := c.miSession, c.workerProcessMIQueryQuery
if miSession == nil {
return nil
}
return func() ([]WorkerProcess, error) {
var workerProcesses []WorkerProcess
err := miSession.Query(&workerProcesses, mi.NamespaceRootWebAdministration, miQuery, maxScrapeDuration)
return workerProcesses, err
}
}
// resolveAppPools maps IIS worker process IDs to application pool names.
//
// The command line of the worker process is the primary source: it needs no
// optional feature and no WMI round trip. WMI is queried at most once, for the
// worker processes whose command line couldn't be read or has no -ap argument,
// and only if queryWMI isn't nil. Unresolved processes have no entry.
func resolveAppPools(
logger *slog.Logger,
pids []uint32,
readCommandLine func(pid uint32) (string, error),
queryWMI func() ([]WorkerProcess, error),
) (map[uint32]string, error) {
appPools := make(map[uint32]string, len(pids))
var unresolved []uint32
for _, pid := range pids {
appPool, err := readCommandLine(pid)
if err == nil {
appPools[pid] = appPool
continue
}
logger.LogAttrs(context.Background(), slog.LevelDebug, "Failed to read the IIS application pool from the worker process command line",
slog.Uint64("pid", uint64(pid)),
slog.Any("err", err),
)
// OpenProcess fails with ERROR_INVALID_PARAMETER for processes that exited after the
// performance counter snapshot. WAS no longer reports them either.
if !errors.Is(err, windows.ERROR_INVALID_PARAMETER) {
unresolved = append(unresolved, pid)
}
}
if len(unresolved) == 0 || queryWMI == nil {
return appPools, nil
}
workerProcesses, err := queryWMI()
if err != nil {
return appPools, fmt.Errorf("WMI query for collector.process.iis failed: %w", err)
}
for _, pid := range unresolved {
for _, wp := range workerProcesses {
if wp.ProcessId == uint64(pid) && wp.AppPoolName != "" {
appPools[pid] = wp.AppPoolName
break
}
}
}
return appPools, nil
}
func (c *Collector) collectWorker(requests <-chan processWorkerRequest) {
defer c.workerWG.Done()
for req := range requests {
(func() {
defer req.waitGroup.Done()
defer func() {
if r := recover(); r != nil {
c.logger.Error("Worker panic",
slog.Any("panic", r),
slog.String("stack", string(debug.Stack())),
)
}
}()
ch := req.ch
name := req.name
data := req.performanceCounterValues
pid := uint64(data.ProcessID)
parentPID := strconv.FormatUint(uint64(data.CreatingProcessID), 10)
if req.appPoolName != "" {
name = name + "_" + req.appPoolName
}
cmdLine, processOwner, processGroupID, err := c.getProcessInformation(uint32(pid))
if err != nil {
c.logger.LogAttrs(context.Background(), slog.LevelDebug, "Failed to get process information",
slog.Uint64("pid", pid),
slog.Any("err", err),
)
}
pidString := strconv.FormatUint(pid, 10)
ch <- prometheus.MustNewConstMetric(
c.info,
prometheus.GaugeValue,
1.0,
name, pidString, parentPID, strconv.Itoa(int(processGroupID)), processOwner, cmdLine,
)
startTime := data.ElapsedTime
if c.config.CounterVersion == 2 {
// For V2, the ElapsedTime is in seconds, so we need to convert it to a timestamp.
// The start time is the current time minus the elapsed time.
startTime = float64(time.Now().UnixMicro())/1e6 - data.ElapsedTime
}
ch <- prometheus.MustNewConstMetric(
c.startTime,
prometheus.GaugeValue,
startTime,
name, pidString,
)
ch <- prometheus.MustNewConstMetric(
c.handleCount,
prometheus.GaugeValue,
data.HandleCount,
name, pidString,
)
ch <- prometheus.MustNewConstMetric(
c.cpuTimeTotal,
prometheus.CounterValue,
data.PercentPrivilegedTime,
name, pidString, "privileged",
)
ch <- prometheus.MustNewConstMetric(
c.cpuTimeTotal,
prometheus.CounterValue,
data.PercentUserTime,
name, pidString, "user",
)
ch <- prometheus.MustNewConstMetric(
c.ioBytesTotal,
prometheus.CounterValue,
data.IoOtherBytesPerSec,
name, pidString, "other",
)
ch <- prometheus.MustNewConstMetric(
c.ioOperationsTotal,
prometheus.CounterValue,
data.IoOtherOperationsPerSec,
name, pidString, "other",
)
ch <- prometheus.MustNewConstMetric(
c.ioBytesTotal,
prometheus.CounterValue,
data.IoReadBytesPerSec,
name, pidString, "read",
)
ch <- prometheus.MustNewConstMetric(
c.ioOperationsTotal,
prometheus.CounterValue,
data.IoReadOperationsPerSec,
name, pidString, "read",
)
ch <- prometheus.MustNewConstMetric(
c.ioBytesTotal,
prometheus.CounterValue,
data.IoWriteBytesPerSec,
name, pidString, "write",
)
ch <- prometheus.MustNewConstMetric(
c.ioOperationsTotal,
prometheus.CounterValue,
data.IoWriteOperationsPerSec,
name, pidString, "write",
)
ch <- prometheus.MustNewConstMetric(
c.pageFaultsTotal,
prometheus.CounterValue,
data.PageFaultsPerSec,
name, pidString,
)
ch <- prometheus.MustNewConstMetric(
c.pageFileBytes,
prometheus.GaugeValue,
data.PageFileBytes,
name, pidString,
)
ch <- prometheus.MustNewConstMetric(
c.poolBytes,
prometheus.GaugeValue,
data.PoolNonPagedBytes,
name, pidString, "nonpaged",
)
ch <- prometheus.MustNewConstMetric(
c.poolBytes,
prometheus.GaugeValue,
data.PoolPagedBytes,
name, pidString, "paged",
)
ch <- prometheus.MustNewConstMetric(
c.priorityBase,
prometheus.GaugeValue,
data.PriorityBase,
name, pidString,
)
ch <- prometheus.MustNewConstMetric(
c.privateBytes,
prometheus.GaugeValue,
data.PrivateBytes,
name, pidString,
)
ch <- prometheus.MustNewConstMetric(
c.threadCount,
prometheus.GaugeValue,
data.ThreadCount,
name, pidString,
)
ch <- prometheus.MustNewConstMetric(
c.virtualBytes,
prometheus.GaugeValue,
data.VirtualBytes,
name, pidString,
)
ch <- prometheus.MustNewConstMetric(
c.workingSetPrivate,
prometheus.GaugeValue,
data.WorkingSetPrivate,
name, pidString,
)
ch <- prometheus.MustNewConstMetric(
c.workingSetPeak,
prometheus.GaugeValue,
data.WorkingSetPeak,
name, pidString,
)
ch <- prometheus.MustNewConstMetric(
c.workingSet,
prometheus.GaugeValue,
data.WorkingSet,
name, pidString,
)
})()
}
}
@@ -1,89 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package process
import (
"log/slog"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestCloseBeforeWorkerStarts(t *testing.T) {
t.Parallel()
c := New(nil)
requests := make(chan processWorkerRequest)
c.workerCh = requests
c.workerWG.Add(1)
closed := make(chan error, 1)
go func() { closed <- c.Close() }()
select {
case _, ok := <-requests:
require.False(t, ok)
case <-time.After(time.Second):
t.Fatal("Close did not close the worker channel")
}
// The worker starts after Close has set c.workerCh to nil.
go c.collectWorker(requests)
select {
case err := <-closed:
require.NoError(t, err)
case <-time.After(time.Second):
t.Fatal("Close did not wait for the delayed worker to exit")
}
require.NoError(t, c.Close())
}
func TestWorkerContinuesAfterRequestPanic(t *testing.T) {
t.Parallel()
c := New(nil)
c.logger = slog.New(slog.DiscardHandler)
c.workerCh = make(chan processWorkerRequest, 2)
c.workerWG.Add(1)
go c.collectWorker(c.workerCh)
var requests sync.WaitGroup
requests.Add(2)
for range 2 {
// Missing descriptors make metric construction panic in both requests.
c.workerCh <- processWorkerRequest{waitGroup: &requests}
}
processed := make(chan struct{})
go func() { requests.Wait(); close(processed) }()
select {
case <-processed:
case <-time.After(time.Second):
t.Fatal("worker did not recover and process the next request")
}
require.NoError(t, c.Close())
}
@@ -0,0 +1,179 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package process
import (
"errors"
"fmt"
"strings"
"unsafe"
"golang.org/x/sys/windows"
)
// maxSnapshotAttempts bounds the retries when processes are created
// between the size query and the snapshot.
const maxSnapshotAttempts = 5
// processSnapshot is one process from SystemProcessInformation.
// The values are the same that the perflib Process counter set reads.
type processSnapshot struct {
name string
pid uint32
parentPID uint32
// createTime is the FILETIME of the process creation, in 100ns intervals since 1601.
createTime int64
// userTime and kernelTime are in 100ns intervals.
userTime int64
kernelTime int64
basePriority int32
handleCount uint32
threadCount uint32
pageFaultCount uint32
virtualSize uint64
workingSetSize uint64
workingSetPeak uint64
workingSetPrivate uint64
pagedPoolUsage uint64
nonPagedPoolUsage uint64
pageFileUsage uint64
privatePageCount uint64
readOperationCount uint64
writeOperationCount uint64
otherOperationCount uint64
readTransferCount uint64
writeTransferCount uint64
otherTransferCount uint64
}
// snapshotBuffer holds the SystemProcessInformation buffer across scrapes.
// The backing array is []uint64, so the process entries are 8-byte aligned.
type snapshotBuffer struct {
buf []uint64
}
// query returns all processes of the system with one NtQuerySystemInformation call.
func (s *snapshotBuffer) query(dst []processSnapshot) ([]processSnapshot, error) {
if len(s.buf) == 0 {
s.buf = make([]uint64, 256<<10/8)
}
var returnLength uint32
for attempt := 0; ; attempt++ {
err := windows.NtQuerySystemInformation(
windows.SystemProcessInformation,
unsafe.Pointer(&s.buf[0]),
uint32(len(s.buf)*8),
&returnLength,
)
if err == nil {
break
}
if !errors.Is(err, windows.STATUS_INFO_LENGTH_MISMATCH) || attempt >= maxSnapshotAttempts {
return dst, fmt.Errorf("NtQuerySystemInformation(SystemProcessInformation): %w", err)
}
// Processes can start before the next call. Leave room for them.
size := max(uint64(returnLength), uint64(len(s.buf))*8)
s.buf = make([]uint64, (size+size/4)/8+1)
}
return parseSystemProcessInformation(dst[:0], unsafe.Slice((*byte)(unsafe.Pointer(&s.buf[0])), len(s.buf)*8), returnLength)
}
// parseSystemProcessInformation decodes the entries of a SystemProcessInformation buffer.
// The buffer must be 8-byte aligned. The image names are copied, so dst does not reference buf.
func parseSystemProcessInformation(dst []processSnapshot, buf []byte, length uint32) ([]processSnapshot, error) {
end := min(uint64(length), uint64(len(buf)))
entrySize := uint64(unsafe.Sizeof(windows.SYSTEM_PROCESS_INFORMATION{}))
for offset := uint64(0); ; {
if offset+entrySize > end {
return dst, fmt.Errorf("SystemProcessInformation entry at offset %d exceeds the buffer length %d", offset, end)
}
entry := (*windows.SYSTEM_PROCESS_INFORMATION)(unsafe.Pointer(&buf[offset]))
var imageName string
if entry.ImageName.Length > 0 && entry.ImageName.Buffer != nil {
// The kernel stores the image name inside the buffer. Check it, so a bad entry cannot read outside of it.
nameOffset := uint64(uintptr(unsafe.Pointer(entry.ImageName.Buffer)) - uintptr(unsafe.Pointer(&buf[0])))
if nameOffset >= end || uint64(entry.ImageName.Length) > end-nameOffset {
return dst, fmt.Errorf("SystemProcessInformation image name at offset %d exceeds the buffer length %d", nameOffset, end)
}
imageName = entry.ImageName.String()
}
dst = append(dst, processSnapshot{
name: processName(uint32(entry.UniqueProcessID), imageName),
pid: uint32(entry.UniqueProcessID),
parentPID: uint32(entry.InheritedFromUniqueProcessID),
createTime: entry.CreateTime,
userTime: entry.UserTime,
kernelTime: entry.KernelTime,
basePriority: entry.BasePriority,
handleCount: entry.HandleCount,
threadCount: entry.NumberOfThreads,
pageFaultCount: entry.PageFaultCount,
virtualSize: uint64(entry.VirtualSize),
workingSetSize: uint64(entry.WorkingSetSize),
workingSetPeak: uint64(entry.PeakWorkingSetSize),
workingSetPrivate: uint64(entry.WorkingSetPrivateSize),
pagedPoolUsage: uint64(entry.QuotaPagedPoolUsage),
nonPagedPoolUsage: uint64(entry.QuotaNonPagedPoolUsage),
pageFileUsage: uint64(entry.PagefileUsage),
privatePageCount: uint64(entry.PrivatePageCount),
readOperationCount: uint64(entry.ReadOperationCount),
writeOperationCount: uint64(entry.WriteOperationCount),
otherOperationCount: uint64(entry.OtherOperationCount),
readTransferCount: uint64(entry.ReadTransferCount),
writeTransferCount: uint64(entry.WriteTransferCount),
otherTransferCount: uint64(entry.OtherTransferCount),
})
if entry.NextEntryOffset == 0 {
return dst, nil
}
offset += uint64(entry.NextEntryOffset)
}
}
// processName returns the instance name that the perflib Process counter set uses:
// the image name without a case-insensitive ".exe" suffix, and "Idle" for the System Idle Process.
// Other extensions, like ".scr", are kept.
func processName(pid uint32, imageName string) string {
if pid == 0 && imageName == "" {
return "Idle"
}
if len(imageName) > len(".exe") && strings.EqualFold(imageName[len(imageName)-len(".exe"):], ".exe") {
return imageName[:len(imageName)-len(".exe")]
}
return imageName
}
@@ -0,0 +1,195 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package process
import (
"log/slog"
"os"
"testing"
"unsafe"
"github.com/stretchr/testify/require"
"golang.org/x/sys/windows"
)
func TestProcessName(t *testing.T) {
t.Parallel()
// The names follow the instances of the perflib Process counter set.
for _, tc := range []struct {
pid uint32
imageName string
want string
}{
{0, "", "Idle"},
{4, "System", "System"},
{100, "Registry", "Registry"},
{200, "svchost.exe", "svchost"},
{300, "EPSDNMON.EXE", "EPSDNMON"},
{400, "Microsoft.Photos.exe", "Microsoft.Photos"},
{500, "screensaver.scr", "screensaver.scr"},
{600, ".exe", ".exe"},
} {
require.Equal(t, tc.want, processName(tc.pid, tc.imageName), "pid %d image %q", tc.pid, tc.imageName)
}
}
func TestSnapshotContainsCurrentProcess(t *testing.T) {
t.Parallel()
var s snapshotBuffer
processes, err := s.query(nil)
require.NoError(t, err)
require.NotEmpty(t, processes)
self := findProcess(t, processes, uint32(os.Getpid()))
require.Equal(t, uint32(os.Getppid()), self.parentPID)
require.Equal(t, currentProcessCreateTime(t), self.createTime)
require.NotZero(t, self.threadCount)
require.NotZero(t, self.handleCount)
require.NotZero(t, self.workingSetSize)
require.NotZero(t, self.privatePageCount)
idle := findProcess(t, processes, 0)
require.Equal(t, "Idle", idle.name)
}
func TestSnapshotGrowsBuffer(t *testing.T) {
t.Parallel()
s := snapshotBuffer{buf: make([]uint64, 1)}
processes, err := s.query(nil)
require.NoError(t, err)
require.NotEmpty(t, processes)
require.Greater(t, len(s.buf), 1)
}
func TestParseSystemProcessInformationRejectsTruncatedBuffer(t *testing.T) {
t.Parallel()
entrySize := int(unsafe.Sizeof(windows.SYSTEM_PROCESS_INFORMATION{}))
buf := make([]uint64, entrySize/8+1)
data := unsafe.Slice((*byte)(unsafe.Pointer(&buf[0])), len(buf)*8)
// The first entry points to a second entry behind the end of the buffer.
(*windows.SYSTEM_PROCESS_INFORMATION)(unsafe.Pointer(&data[0])).NextEntryOffset = uint32(entrySize)
processes, err := parseSystemProcessInformation(nil, data, uint32(len(data)))
require.Error(t, err)
require.Len(t, processes, 1)
_, err = parseSystemProcessInformation(nil, data, uint32(entrySize-1))
require.Error(t, err)
}
func TestParseSystemProcessInformationRejectsImageNameOutsideBuffer(t *testing.T) {
t.Parallel()
entrySize := int(unsafe.Sizeof(windows.SYSTEM_PROCESS_INFORMATION{}))
buf := make([]uint64, entrySize/8+1)
data := unsafe.Slice((*byte)(unsafe.Pointer(&buf[0])), len(buf)*8)
other := []uint16{'a', 'b'}
entry := (*windows.SYSTEM_PROCESS_INFORMATION)(unsafe.Pointer(&data[0]))
entry.ImageName.Buffer = &other[0]
entry.ImageName.Length = 4
_, err := parseSystemProcessInformation(nil, data, uint32(len(data)))
require.Error(t, err)
// A name inside the buffer, but longer than the rest of it.
entry.ImageName.Buffer = (*uint16)(unsafe.Pointer(&data[len(data)-2]))
_, err = parseSystemProcessInformation(nil, data, uint32(len(data)))
require.Error(t, err)
}
func TestResolveProcessInfoCache(t *testing.T) {
t.Parallel()
c := New(nil)
c.logger = slog.New(slog.DiscardHandler)
var s snapshotBuffer
processes, err := s.query(nil)
require.NoError(t, err)
self := findProcess(t, processes, uint32(os.Getpid()))
exited := processSnapshot{pid: 1<<32 - 4, createTime: 1}
infos := c.resolveProcessInfo([]*processSnapshot{&self, &exited})
require.NotEmpty(t, infos[0].owner)
require.NotEmpty(t, infos[0].cmdLine)
require.Contains(t, c.infoCache, self.pid)
require.NotContains(t, c.infoCache, exited.pid, "failed lookups must be retried")
// A cached entry with the same creation time is reused without opening the process.
c.infoCache[self.pid] = processInfo{createTime: self.createTime, owner: "cached"}
infos = c.resolveProcessInfo([]*processSnapshot{&self})
require.Equal(t, "cached", infos[0].owner)
// A reused PID has another creation time and is looked up again.
c.infoCache[self.pid] = processInfo{createTime: self.createTime - 1, owner: "cached"}
infos = c.resolveProcessInfo([]*processSnapshot{&self})
require.NotEqual(t, "cached", infos[0].owner)
// Processes that are gone are evicted.
c.resolveProcessInfo(nil)
require.Empty(t, c.infoCache)
}
func TestGetProcessInformationDetectsReusedPID(t *testing.T) {
t.Parallel()
c := New(nil)
c.logger = slog.New(slog.DiscardHandler)
_, _, _, err := c.getProcessInformation(uint32(os.Getpid()), currentProcessCreateTime(t)+1)
require.ErrorIs(t, err, errProcessGone)
cmdLine, owner, _, err := c.getProcessInformation(uint32(os.Getpid()), currentProcessCreateTime(t))
require.NoError(t, err)
require.NotEmpty(t, cmdLine)
require.NotEmpty(t, owner)
}
func findProcess(t *testing.T, processes []processSnapshot, pid uint32) processSnapshot {
t.Helper()
for _, process := range processes {
if process.pid == pid {
return process
}
}
require.FailNow(t, "process not found in snapshot", "pid %d", pid)
return processSnapshot{}
}
func currentProcessCreateTime(t *testing.T) int64 {
t.Helper()
var creationTime, exitTime, kernelTime, userTime windows.Filetime
require.NoError(t, windows.GetProcessTimes(windows.CurrentProcess(), &creationTime, &exitTime, &kernelTime, &userTime))
return int64(creationTime.HighDateTime)<<32 | int64(creationTime.LowDateTime)
}
-33
View File
@@ -21,36 +21,3 @@ type WorkerProcess struct {
AppPoolName string `mi:"AppPoolName"`
ProcessId uint64 `mi:"ProcessId"`
}
type perfDataCounterValues struct {
Name string
PercentProcessorTime float64 `perfdata:"% Processor Time"`
PercentPrivilegedTime float64 `perfdata:"% Privileged Time"`
PercentUserTime float64 `perfdata:"% User Time"`
CreatingProcessID float64 `perfdata:"Creating Process ID"`
ElapsedTime float64 `perfdata:"Elapsed Time"`
HandleCount float64 `perfdata:"Handle Count"`
IoDataBytesPerSec float64 `perfdata:"IO Data Bytes/sec"`
IoDataOperationsPerSec float64 `perfdata:"IO Data Operations/sec"`
IoOtherBytesPerSec float64 `perfdata:"IO Other Bytes/sec"`
IoOtherOperationsPerSec float64 `perfdata:"IO Other Operations/sec"`
IoReadBytesPerSec float64 `perfdata:"IO Read Bytes/sec"`
IoReadOperationsPerSec float64 `perfdata:"IO Read Operations/sec"`
IoWriteBytesPerSec float64 `perfdata:"IO Write Bytes/sec"`
IoWriteOperationsPerSec float64 `perfdata:"IO Write Operations/sec"`
PageFaultsPerSec float64 `perfdata:"Page Faults/sec"`
PageFileBytesPeak float64 `perfdata:"Page File Bytes Peak"`
PageFileBytes float64 `perfdata:"Page File Bytes"`
PoolNonPagedBytes float64 `perfdata:"Pool Nonpaged Bytes"`
PoolPagedBytes float64 `perfdata:"Pool Paged Bytes"`
PriorityBase float64 `perfdata:"Priority Base"`
PrivateBytes float64 `perfdata:"Private Bytes"`
ThreadCount float64 `perfdata:"Thread Count"`
VirtualBytesPeak float64 `perfdata:"Virtual Bytes Peak"`
VirtualBytes float64 `perfdata:"Virtual Bytes"`
WorkingSetPrivate float64 `perfdata:"Working Set - Private"`
WorkingSetPeak float64 `perfdata:"Working Set Peak"`
WorkingSet float64 `perfdata:"Working Set"`
ProcessID float64 `perfdata:"Process ID" perfdata_v1:"ID Process"`
}
+14 -23
View File
@@ -26,7 +26,6 @@ import (
"time"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"github.com/prometheus-community/windows_exporter/internal/pdh/registry"
"github.com/stretchr/testify/require"
)
@@ -78,30 +77,22 @@ func TestTotalSuffixProcess(t *testing.T) {
ID float64 `perfdata:"ID Process"`
}
legacy, err := registry.NewCollector[processValues]("Process", pdh.InstancesAll)
collector, err := pdh.NewCollector[processValues](slog.New(slog.DiscardHandler), pdh.CounterTypeRaw, "Process", []string{"worker_Total"})
require.NoError(t, err)
t.Cleanup(legacy.Close)
t.Cleanup(collector.Close)
modern, err := pdh.NewCollector[processValues](slog.New(slog.DiscardHandler), pdh.CounterTypeRaw, "Process", []string{"worker_Total"})
require.NoError(t, err)
t.Cleanup(modern.Close)
require.Eventually(t, func() bool {
var rows []processValues
if collector.Collect(&rows) != nil {
return false
}
for name, collect := range map[string]func(*[]processValues) error{"registry": legacy.Collect, "explicit PDH": modern.Collect} {
t.Run(name, func(t *testing.T) {
require.Eventually(t, func() bool {
var rows []processValues
if collect(&rows) != nil {
return false
}
for _, row := range rows {
if row.ID == float64(child.Process.Pid) && row.Name == "worker_Total" {
return true
}
}
for _, row := range rows {
if row.ID == float64(child.Process.Pid) && row.Name == "worker_Total" {
return true
}
}
return false
}, time.Second*5, time.Millisecond*100)
})
}
return false
}, time.Second*5, time.Millisecond*100)
}
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2018 Leopold Schabel / The perflib_exporter authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-229
View File
@@ -1,229 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package registry
import (
"errors"
"fmt"
"reflect"
"strings"
"sync"
"github.com/prometheus-community/windows_exporter/internal/pdh"
)
type Collector[T any] struct {
object string
query string
counters map[string]Counter
nameIndexValue int
// bufferMu guards buffer, the raw data buffer of the last query. A Collect
// takes it out for the duration of the query, so concurrent calls never
// share it; they allocate their own buffer instead.
bufferMu sync.Mutex
buffer []byte
}
type Counter struct {
Name string
Desc string
Instances map[string]uint32
Type uint32
Frequency float64
FieldIndexValue int
FieldIndexSecondValue int
}
// NewCollector creates a collector for the counters declared by the perfdata_v1 or perfdata tags of the struct T.
func NewCollector[T any](object string, _ []string) (*Collector[T], error) {
valueType := reflect.TypeFor[T]()
if valueType.Kind() != reflect.Struct {
return nil, fmt.Errorf("expected a struct, got %s", valueType)
}
if err := CounterNameTable.load(); err != nil {
return nil, err
}
query := MapCounterToIndex(object)
if query == "0" {
return nil, fmt.Errorf("perflib object %q not found in the counter name table", object)
}
collector := &Collector[T]{
object: object,
query: query,
nameIndexValue: -1,
counters: make(map[string]Counter),
}
if f, ok := valueType.FieldByName("Name"); ok {
if f.Type.Kind() == reflect.String {
collector.nameIndexValue = f.Index[0]
}
}
for _, f := range reflect.VisibleFields(valueType) {
tag, ok := f.Tag.Lookup("perfdata_v1")
if !ok {
tag, ok = f.Tag.Lookup("perfdata")
if !ok {
continue
}
}
if f.Type.Kind() != reflect.Float64 {
return nil, fmt.Errorf("field %s must be a float64", f.Name)
}
counterName, secondValue := strings.CutSuffix(tag, ",secondvalue")
counter, ok := collector.counters[counterName]
if !ok {
counter = Counter{
Name: counterName,
FieldIndexSecondValue: -1,
FieldIndexValue: -1,
}
}
if secondValue {
counter.FieldIndexSecondValue = f.Index[0]
} else {
counter.FieldIndexValue = f.Index[0]
}
collector.counters[counterName] = counter
}
var collectValues []T
if err := collector.Collect(&collectValues); err != nil {
return nil, fmt.Errorf("failed to collect initial data: %w", err)
}
return collector, nil
}
func (c *Collector[T]) Describe() map[string]string {
return map[string]string{}
}
// Collect replaces the content of dst with one row per collected instance.
func (c *Collector[T]) Collect(dst *[]T) error {
if dst == nil {
return errors.New("dst must not be nil")
}
c.bufferMu.Lock()
buffer := c.buffer
c.buffer = nil
c.bufferMu.Unlock()
perfObjects, buffer, err := queryPerformanceData(c.query, c.object, buffer)
// Keep the larger buffer if a concurrent call put one back in the meantime.
c.bufferMu.Lock()
if cap(buffer) > cap(c.buffer) {
c.buffer = buffer
}
c.bufferMu.Unlock()
if err != nil {
return fmt.Errorf("QueryPerformanceData: %w", err)
}
if len(perfObjects) == 0 || perfObjects[0] == nil || len(perfObjects[0].Instances) == 0 {
return nil
}
*dst = make([]T, 0, len(perfObjects[0].Instances))
for _, perfObject := range perfObjects {
if perfObject.Name != c.object {
continue
}
for _, perfInstance := range perfObject.Instances {
instanceName := perfInstance.Name
if pdh.IsTotalInstance(c.object, instanceName) {
continue
}
if instanceName == "" || instanceName == "*" {
instanceName = pdh.InstanceEmpty
}
var row T
rv := reflect.ValueOf(&row).Elem()
if c.nameIndexValue != -1 {
rv.Field(c.nameIndexValue).SetString(instanceName)
}
for _, perfCounter := range perfInstance.Counters {
if perfCounter.Def.IsBaseValue && !perfCounter.Def.IsNanosecondCounter {
continue
}
counter, ok := c.counters[perfCounter.Def.Name]
if !ok {
continue
}
switch perfCounter.Def.CounterType {
case pdh.PERF_ELAPSED_TIME:
// A zero frequency would divide by zero. The value is left unset.
if perfObject.Frequency <= 0 || counter.FieldIndexValue == -1 {
continue
}
rv.Field(counter.FieldIndexValue).
SetFloat(float64((perfCounter.Value - pdh.WindowsEpoch) / perfObject.Frequency))
case pdh.PERF_100NSEC_TIMER, pdh.PERF_PRECISION_100NS_TIMER:
if counter.FieldIndexValue == -1 {
continue
}
rv.Field(counter.FieldIndexValue).
SetFloat(float64(perfCounter.Value) * pdh.TicksToSecondScaleFactor)
default:
if counter.FieldIndexSecondValue != -1 {
rv.Field(counter.FieldIndexSecondValue).
SetFloat(float64(perfCounter.SecondValue))
}
if counter.FieldIndexValue != -1 {
rv.Field(counter.FieldIndexValue).
SetFloat(float64(perfCounter.Value))
}
}
}
*dst = append(*dst, row)
}
}
return nil
}
func (c *Collector[T]) Close() {}
-125
View File
@@ -1,125 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package registry
import (
"bytes"
"fmt"
"strconv"
"sync"
)
// CounterNameTable Initialize global name tables
// profiling, add option to disable name tables if necessary
// Not sure if we should resolve the names at all or just have the caller do it on demand
// (for many use cases the index is sufficient)
//
//nolint:gochecknoglobals
var CounterNameTable = QueryNameTable("Counter 009")
func (p *perfObjectType) LookupName() string {
return CounterNameTable.LookupString(p.ObjectNameTitleIndex)
}
type NameTable struct {
mu sync.Mutex
name string
table struct {
index map[uint32]string
string map[string]uint32
}
}
// LookupString returns the name for index. It returns an empty string if the
// index is unknown or the name table could not be loaded.
func (t *NameTable) LookupString(index uint32) string {
if t.load() != nil {
return ""
}
return t.table.index[index]
}
// LookupIndex returns the index for str. It returns 0 if the name is unknown
// or the name table could not be loaded.
func (t *NameTable) LookupIndex(str string) uint32 {
if t.load() != nil {
return 0
}
return t.table.string[str]
}
// QueryNameTable Query a perflib name table from the v1. Specify the type and the language
// code (i.e. "Counter 009" or "Help 009") for English language.
func QueryNameTable(tableName string) *NameTable {
return &NameTable{
name: tableName,
}
}
// load reads the name table from the registry. A failed load is retried on the
// next call, so a transient error does not disable the table for good. The
// maps are not modified once loaded.
func (t *NameTable) load() error {
t.mu.Lock()
defer t.mu.Unlock()
if t.table.index != nil {
return nil
}
buffer, err := queryRawData(t.name, nil)
if err != nil {
return fmt.Errorf("failed to query perflib name table %q: %w", t.name, err)
}
t.table.index, t.table.string = parseNameTable(buffer)
return nil
}
// parseNameTable parses a name table, a REG_MULTI_SZ of alternating index and
// name strings. Parsing stops at the first incomplete pair.
func parseNameTable(buffer []byte) (map[uint32]string, map[string]uint32) {
index := make(map[uint32]string)
names := make(map[string]uint32)
r := bytes.NewReader(buffer)
for {
indexStr, err := readUTF16String(r)
if err != nil {
break
}
desc, err := readUTF16String(r)
if err != nil {
break
}
indexInt, _ := strconv.ParseUint(indexStr, 10, 32)
index[uint32(indexInt)] = desc
names[desc] = uint32(indexInt)
}
return index, names
}
-594
View File
@@ -1,594 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package registry
/*
Go bindings for the HKEY_PERFORMANCE_DATA perflib / Performance Counters interface.
# Overview
HKEY_PERFORMANCE_DATA is a low-level alternative to the higher-level PDH library and WMI.
It operates on blocks of counters and only returns raw values without calculating rates
or formatting them, which is exactly what you want for, say, a Prometheus exporter
(not so much for a GUI like Windows Performance Monitor).
Its overhead is much lower than the high-level libraries.
It operates on the same set of perflib providers as PDH and WMI. See this document
for more details on the relationship between the different libraries:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa371643(v=vs.85).aspx
Example C++ source code:
https://msdn.microsoft.com/de-de/library/windows/desktop/aa372138(v=vs.85).aspx
For now, the API is not stable and is probably going to change in future
perflib_exporter releases. If you want to use this library, send the author an email
so we can discuss your requirements and stabilize the API.
# Names
Counter names and help texts are resolved by looking up an index in a name table.
Since Microsoft loves internalization, both names and help texts can be requested
any locally available language.
The library automatically loads the name tables and resolves all identifiers
in English ("Name" and "HelpText" struct members). You can manually resolve
identifiers in a different language by using the NameTable API.
# Performance Counters intro
Windows has a system-wide performance counter mechanism. Most performance counters
are stored as actual counters, not gauges (with some exceptions).
There's additional metadata which defines how the counter should be presented to the user
(for example, as a calculated rate). This library disregards all of the display metadata.
At the top level, there's a number of performance counter objects.
Each object has counter definitions, which contain the metadata for a particular
counter, and either zero or multiple instances. We hide the fact that there are
objects with no instances, and simply return a single null instance.
There's one counter per counter definition and instance (or the object itself, if
there are no instances).
Behind the scenes, every perflib DLL provides one or more objects.
Perflib has a v1 where DLLs are dynamically registered and
unregistered. Some third party applications like VMWare provide their own counters,
but this is, sadly, a rare occurrence.
Different Windows releases have different numbers of counters.
Objects and counters are identified by well-known indices.
Here's an example object with one instance:
4320 WSMan Quota Statistics [7 counters, 1 instance(s)]
`-- "WinRMService"
`-- Total Requests/Second [4322] = 59
`-- User Quota Violations/Second [4324] = 0
`-- System Quota Violations/Second [4326] = 0
`-- Active Shells [4328] = 0
`-- Active Operations [4330] = 0
`-- Active Users [4332] = 0
`-- Process ID [4334] = 928
All "per second" metrics are counters, the rest are gauges.
Another example, with no instance:
4600 Network QoS Policy [6 counters, 1 instance(s)]
`-- (default)
`-- Packets transmitted [4602] = 1744
`-- Packets transmitted/sec [4604] = 4852
`-- Bytes transmitted [4606] = 4853
`-- Bytes transmitted/sec [4608] = 180388626632
`-- Packets dropped [4610] = 0
`-- Packets dropped/sec [4612] = 0
You can access the same values using PowerShell's Get-Counter cmdlet
or the Performance Monitor.
> Get-Counter '\WSMan Quota Statistics(WinRMService)\Process ID'
Timestamp CounterSamples
--------- --------------
1/28/2018 10:18:00 PM \\DEV\wsman quota statistics(winrmservice)\process id :
928
> (Get-Counter '\Process(Idle)\% Processor Time').CounterSamples[0] | Format-List *
[..detailed output...]
Data for some of the objects is also available through WMI:
> Get-CimInstance Win32_PerfRawData_Counters_WSManQuotaStatistics
Name : WinRMService
[...]
ActiveOperations : 0
ActiveShells : 0
ActiveUsers : 0
ProcessID : 928
SystemQuotaViolationsPerSecond : 0
TotalRequestsPerSecond : 59
UserQuotaViolationsPerSecond : 0
*/
import (
"encoding/binary"
"errors"
"fmt"
"strings"
"time"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"golang.org/x/sys/windows"
)
// There's a LittleEndian field in the PERF header - we ought to check it.
//
//nolint:gochecknoglobals
var bo = binary.LittleEndian
// PerfObject Top-level performance object (like "Process").
type PerfObject struct {
Name string
// NameIndex Same index you pass to QueryPerformanceData
NameIndex uint
Instances []PerfInstance
CounterDefs []PerfCounterDef
Frequency int64
}
// PerfInstance Each object can have multiple instances. For example,
// In case the object has no instances, we return one single PerfInstance with an empty name.
type PerfInstance struct {
// *not* resolved using a name table
Name string
// Counters holds one counter per counter definition of the object, in the
// same order. The counters of all instances share one backing array.
Counters []PerfCounter
}
type PerfCounterDef struct {
Name string
NameIndex uint
// For debugging - subject to removal. CounterType is a perflib
// implementation detail (see perflib.h) and should not be used outside
// of this package. We export it so we can show it on /dump.
CounterType uint32
// PERF_TYPE_COUNTER (otherwise, it's a gauge)
IsCounter bool
// PERF_COUNTER_BASE (base value of a multi-value fraction)
IsBaseValue bool
// PERF_TIMER_100NS
IsNanosecondCounter bool
HasSecondValue bool
rawData perfCounterDefinition
}
type PerfCounter struct {
Value int64
Def *PerfCounterDef
SecondValue int64
}
//nolint:gochecknoglobals
var (
bufLenGlobal = uint32(400000)
bufLenCostly = uint32(2000000)
)
// queryRawData Queries the performance counter buffer using RegQueryValueEx, returning raw bytes. See:
// https://msdn.microsoft.com/de-de/library/windows/desktop/aa373219(v=vs.85).aspx
//
// If buffer has enough capacity for the initial size guess, it is reused instead
// of allocating a new one. The returned slice may share memory with buffer.
func queryRawData(query string, buffer []byte) ([]byte, error) {
var (
valType uint32
bufLen uint32
)
switch query {
case "Global":
bufLen = bufLenGlobal
case "Costly":
bufLen = bufLenCostly
default:
// depends on the number of values requested
// need make an educated guess
numCounters := len(strings.Split(query, " "))
bufLen = uint32(150000 * numCounters)
}
// A buffer from a previous call keeps any growth from ERROR_MORE_DATA, so
// using its full capacity avoids growing it again.
if uint32(cap(buffer)) >= bufLen {
buffer = buffer[:cap(buffer)]
} else {
buffer = make([]byte, bufLen)
}
name, err := windows.UTF16PtrFromString(query)
if err != nil {
return nil, fmt.Errorf("failed to encode query string: %w", err)
}
for {
bufLen := uint32(len(buffer))
err := windows.RegQueryValueEx(
windows.HKEY_PERFORMANCE_DATA,
name,
nil,
&valType,
(*byte)(unsafe.Pointer(&buffer[0])),
&bufLen)
switch {
case errors.Is(err, error(windows.ERROR_MORE_DATA)):
// Exponential buffer growth prevents O(N) allocation spin-loops under heavy load.
// The previous copy() was removed because the buffer contents are
// incomplete/invalid and will be overwritten on the next API call.
buffer = make([]byte, len(buffer)*2)
continue
case errors.Is(err, error(windows.ERROR_BUSY)):
time.Sleep(50 * time.Millisecond)
continue
case err != nil:
if errNo, ok := errors.AsType[windows.Errno](err); ok {
return nil, fmt.Errorf("ReqQueryValueEx failed: %w errno %d", err, uint(errNo))
}
return nil, err
}
buffer = buffer[:bufLen]
switch query {
case "Global":
if bufLen > bufLenGlobal {
bufLenGlobal = bufLen
}
case "Costly":
if bufLen > bufLenCostly {
bufLenCostly = bufLen
}
}
return buffer, nil
}
}
/*
QueryPerformanceData Query all performance counters that match a given query.
The query can be any of the following:
- "Global" (all performance counters except those Windows marked as costly)
- "Costly" (only the costly ones)
- One or more object indices, separated by spaces ("238 2 5")
Many objects have dependencies - if you query one of them, you often get back
more than you asked for.
*/
func QueryPerformanceData(query string, counterName string) ([]*PerfObject, error) {
objects, _, err := queryPerformanceData(query, counterName, nil)
return objects, err
}
// queryPerformanceData is QueryPerformanceData with a reusable raw data buffer.
// It returns the buffer for the next call. The parsed objects copy everything
// they need out of the buffer, so the buffer can be reused once this returns.
func queryPerformanceData(query string, counterName string, buffer []byte) ([]*PerfObject, []byte, error) {
// Object and counter names are resolved through the name table.
if err := CounterNameTable.load(); err != nil {
return nil, buffer, err
}
buffer, err := queryRawData(query, buffer)
if err != nil {
return nil, buffer, err
}
objects, err := parsePerformanceData(buffer, counterName)
if err != nil {
return nil, buffer, fmt.Errorf("failed to parse performance data for %q: %w", query, err)
}
return objects, buffer, nil
}
//nolint:gochecknoglobals
var (
perfDataBlockSize = int64(binary.Size(perfDataBlock{}))
perfObjectTypeSize = int64(binary.Size(perfObjectType{}))
perfCounterDefinitionSize = int64(binary.Size(perfCounterDefinition{}))
perfInstanceDefinitionSize = int64(binary.Size(perfInstanceDefinition{}))
perfCounterBlockSize = int64(binary.Size(perfCounterBlock{}))
)
// errMalformedPerformanceData is returned when the performance data contradicts
// itself, e.g. it declares more objects than fit into the buffer. Counts,
// offsets and lengths are checked before they size an allocation or index into
// the buffer.
var errMalformedPerformanceData = errors.New("malformed performance data")
// parsePerformanceData parses a PERF_DATA_BLOCK as returned by queryRawData.
// If counterName is set, only the first object with that name is returned.
//
// The structures are decoded straight from buffer. The parsed objects copy
// everything they keep, so they do not reference buffer.
func parsePerformanceData(buffer []byte, counterName string) ([]*PerfObject, error) {
bufLen := int64(len(buffer))
// Read global header
var header perfDataBlock
if err := header.decode(buffer); err != nil {
return nil, fmt.Errorf("failed to read performance data block: %w", err)
}
// Check for "PERF" signature
if header.Signature != [4]uint16{80, 69, 82, 70} {
return nil, fmt.Errorf("%w: invalid performance block signature %v", errMalformedPerformanceData, header.Signature)
}
// Parse the performance data
numObjects := int64(header.NumObjectTypes)
objOffset := int64(header.HeaderLength)
if objOffset < perfDataBlockSize || numObjects*perfObjectTypeSize > bufLen-objOffset {
return nil, fmt.Errorf("%w: %d objects at offset %d do not fit into %d bytes", errMalformedPerformanceData, numObjects, objOffset, bufLen)
}
numFilteredObjects := 0
objects := make([]*PerfObject, numObjects)
// nameScratch holds the UTF16 code units of an instance name while it is decoded.
var nameScratch []uint16
for i := range numObjects {
var obj perfObjectType
if err := obj.decode(bytesAt(buffer, objOffset)); err != nil {
return nil, err
}
perfCounterName := obj.LookupName()
if counterName != "" && perfCounterName != counterName {
objOffset += int64(obj.TotalByteLength)
continue
}
numCounterDefs := int64(obj.NumCounters)
numInstances := int64(obj.NumInstances)
if numCounterDefs*perfCounterDefinitionSize > bufLen-objOffset-perfObjectTypeSize {
return nil, fmt.Errorf("%w: %d counter definitions do not fit into the buffer", errMalformedPerformanceData, numCounterDefs)
}
// Every instance has at least an instance definition and a counter block.
if numInstances*(perfInstanceDefinitionSize+perfCounterBlockSize) > bufLen-objOffset {
return nil, fmt.Errorf("%w: %d instances do not fit into the buffer", errMalformedPerformanceData, numInstances)
}
// Perf objects can have no instances. The perflib differentiates
// between objects with instances and without, but we just create
// an empty instance in order to simplify the interface.
if numInstances <= 0 {
numInstances = 1
}
// The counters of all instances share one backing array below, which
// grows with instances × counters. Counter values do not overlap, so
// each takes at least one byte of the object. Checking that keeps the
// allocation linear in the buffer size for malformed data.
if numInstances*numCounterDefs > bufLen-objOffset {
return nil, fmt.Errorf("%w: %d instances with %d counters each do not fit into the buffer", errMalformedPerformanceData, numInstances, numCounterDefs)
}
instances := make([]PerfInstance, numInstances)
counterDefs := make([]PerfCounterDef, numCounterDefs)
counters := make([]PerfCounter, numInstances*numCounterDefs)
objects[i] = &PerfObject{
Name: perfCounterName,
NameIndex: uint(obj.ObjectNameTitleIndex),
Instances: instances,
CounterDefs: counterDefs,
Frequency: obj.PerfFreq,
}
// The counter definitions follow the object header.
defOffset := objOffset + perfObjectTypeSize
for j := range numCounterDefs {
var def perfCounterDefinition
if err := def.decode(bytesAt(buffer, defOffset+j*perfCounterDefinitionSize)); err != nil {
return nil, err
}
counterDefs[j] = PerfCounterDef{
Name: def.LookupName(),
NameIndex: uint(def.CounterNameTitleIndex),
rawData: def,
CounterType: def.CounterType,
IsCounter: def.CounterType&0x400 == 0x400,
IsBaseValue: def.CounterType&0x00030000 == 0x00030000,
IsNanosecondCounter: def.CounterType&0x00100000 == 0x00100000,
HasSecondValue: def.CounterType == pdh.PERF_AVERAGE_BULK,
}
}
if obj.NumInstances <= 0 { //nolint:nestif
blockOffset := objOffset + int64(obj.DefinitionLength)
if _, err := parseCounterBlock(buffer, blockOffset, counterDefs, counters); err != nil {
return nil, err
}
instances[0].Counters = counters
} else {
instOffset := objOffset + int64(obj.DefinitionLength)
for j := range numInstances {
var inst perfInstanceDefinition
if err := inst.decode(bytesAt(buffer, instOffset)); err != nil {
return nil, err
}
namePos := instOffset + int64(inst.NameOffset)
if namePos+int64(inst.NameLength) > bufLen {
return nil, fmt.Errorf("%w: instance name at offset %d with length %d exceeds the buffer", errMalformedPerformanceData, namePos, inst.NameLength)
}
instances[j].Name, nameScratch = decodeUTF16String(buffer[namePos:namePos+int64(inst.NameLength)], nameScratch)
blockOffset := instOffset + int64(inst.ByteLength)
instCounters := counters[j*numCounterDefs : (j+1)*numCounterDefs : (j+1)*numCounterDefs]
blockLength, err := parseCounterBlock(buffer, blockOffset, counterDefs, instCounters)
if err != nil {
return nil, err
}
instances[j].Counters = instCounters
instOffset = blockOffset + blockLength
}
}
if counterName != "" {
return objects[i : i+1], nil
}
// Next perfObjectType
objOffset += int64(obj.TotalByteLength)
numFilteredObjects++
}
return objects[:numFilteredObjects], nil
}
// bytesAt returns b from offset pos on, or nil if pos lies outside of b.
func bytesAt(b []byte, pos int64) []byte {
if pos < 0 || pos > int64(len(b)) {
return nil
}
return b[pos:]
}
// parseCounterBlock reads the PERF_COUNTER_BLOCK at pos into counters, which
// has one element per definition in defs. It returns the length of the block.
func parseCounterBlock(b []byte, pos int64, defs []PerfCounterDef, counters []PerfCounter) (int64, error) {
var block perfCounterBlock
if err := block.decode(bytesAt(b, pos)); err != nil {
return 0, err
}
// The block length includes the length field itself. A shorter block would
// make the next instance overlap this one.
if int64(block.ByteLength) < perfCounterBlockSize {
return 0, fmt.Errorf("%w: counter block at offset %d has length %d", errMalformedPerformanceData, pos, block.ByteLength)
}
for i := range defs {
def := &defs[i]
valueOffset := pos + int64(def.rawData.CounterOffset)
size := counterValueSize(&def.rawData)
end := valueOffset + size
if def.HasSecondValue {
end = valueOffset + 8 + size
}
if end > int64(len(b)) {
return 0, fmt.Errorf("%w: value of counter %s at offset %d exceeds the buffer", errMalformedPerformanceData, def.Name, valueOffset)
}
value := convertCounterValue(&def.rawData, b, valueOffset)
secondValue := int64(0)
if def.HasSecondValue {
secondValue = convertCounterValue(&def.rawData, b, valueOffset+8)
}
counters[i] = PerfCounter{
Value: value,
Def: def,
SecondValue: secondValue,
}
}
return int64(block.ByteLength), nil
}
// counterValueSize returns the number of bytes convertCounterValue reads.
func counterValueSize(counterDef *perfCounterDefinition) int64 {
if counterDef.CounterSize == 8 {
return 8
}
return 4
}
func convertCounterValue(counterDef *perfCounterDefinition, buffer []byte, valueOffset int64) int64 {
/*
We can safely ignore the type since we're not interested in anything except the raw value.
We also ignore all of the other attributes (timestamp, presentation, multi counter values...)
See also: winperf.h.
Here's the most common value for CounterType:
65536 32bit counter
65792 64bit counter
272696320 32bit rate
272696576 64bit rate
*/
switch counterDef.CounterSize {
case 4:
return int64(bo.Uint32(buffer[valueOffset:(valueOffset + 4)]))
case 8:
return int64(bo.Uint64(buffer[valueOffset:(valueOffset + 8)]))
default:
return int64(bo.Uint32(buffer[valueOffset:(valueOffset + 4)]))
}
}
-425
View File
@@ -1,425 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package registry
import (
"bytes"
"encoding/binary"
"io"
"testing"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"github.com/stretchr/testify/require"
"golang.org/x/sys/windows"
)
// syntheticPerfData describes a PERF_DATA_BLOCK with one object and one
// 64-bit counter, either without instances or with a single instance.
type syntheticPerfData struct {
header perfDataBlock
object perfObjectType
def perfCounterDefinition
instance perfInstanceDefinition
instanceName []uint16
block perfCounterBlock
value uint64
withInstance bool
}
func newSyntheticPerfData(withInstance bool) *syntheticPerfData {
d := &syntheticPerfData{
withInstance: withInstance,
instanceName: windows.StringToUTF16("inst"),
value: 42,
}
d.header.Signature = [4]uint16{'P', 'E', 'R', 'F'}
d.header.LittleEndian = 1
d.header.HeaderLength = uint32(perfDataBlockSize)
d.header.NumObjectTypes = 1
d.object.HeaderLength = uint32(perfObjectTypeSize)
d.object.DefinitionLength = uint32(perfObjectTypeSize + perfCounterDefinitionSize)
d.object.NumCounters = 1
d.object.NumInstances = -1 // PERF_NO_INSTANCES
d.object.PerfFreq = 10_000_000
d.def.ByteLength = uint32(perfCounterDefinitionSize)
d.def.CounterType = pdh.PERF_COUNTER_LARGE_RAWCOUNT
d.def.CounterSize = 8
// The value follows the 4-byte block length and 4 bytes of padding.
d.def.CounterOffset = 8
d.block.ByteLength = 16
if withInstance {
nameLength := uint32(len(d.instanceName) * 2)
d.object.NumInstances = 1
d.instance.NameOffset = uint32(perfInstanceDefinitionSize)
d.instance.NameLength = nameLength
// The name is padded to 8 bytes.
d.instance.ByteLength = uint32(perfInstanceDefinitionSize) + (nameLength+7)&^7
}
return d
}
func (d *syntheticPerfData) bytes(tb testing.TB) []byte {
tb.Helper()
var body bytes.Buffer
write := func(v any) {
require.NoError(tb, binary.Write(&body, bo, v))
}
write(d.def)
if d.withInstance {
write(d.instance)
write(d.instanceName)
body.Write(make([]byte, int(d.instance.ByteLength)-int(perfInstanceDefinitionSize)-len(d.instanceName)*2))
}
write(d.block)
body.Write(make([]byte, 4))
write(d.value)
object := d.object
if object.TotalByteLength == 0 {
object.TotalByteLength = uint32(perfObjectTypeSize) + uint32(body.Len())
}
header := d.header
if header.TotalByteLength == 0 {
header.TotalByteLength = uint32(perfDataBlockSize) + object.TotalByteLength
}
var buf bytes.Buffer
require.NoError(tb, binary.Write(&buf, bo, header))
require.NoError(tb, binary.Write(&buf, bo, object))
buf.Write(body.Bytes())
return buf.Bytes()
}
func TestParsePerformanceData(t *testing.T) {
t.Parallel()
for _, withInstance := range []bool{false, true} {
objects, err := parsePerformanceData(newSyntheticPerfData(withInstance).bytes(t), "")
require.NoError(t, err)
require.Len(t, objects, 1)
require.Len(t, objects[0].Instances, 1)
require.Len(t, objects[0].Instances[0].Counters, 1)
require.Equal(t, int64(42), objects[0].Instances[0].Counters[0].Value)
if withInstance {
require.Equal(t, "inst", objects[0].Instances[0].Name)
}
}
}
// TestParsePerformanceDataMalformed checks that inconsistent performance data
// is reported as an error. The parser runs during Build of the process
// collector, where a panic would end the exporter.
func TestParsePerformanceDataMalformed(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
withInstance bool
mutate func(d *syntheticPerfData)
}{
{"invalid signature", false, func(d *syntheticPerfData) { d.header.Signature[0] = 'X' }},
{"header length too short", false, func(d *syntheticPerfData) { d.header.HeaderLength = 0 }},
{"too many objects", false, func(d *syntheticPerfData) { d.header.NumObjectTypes = 1 << 31 }},
{"too many counters", false, func(d *syntheticPerfData) { d.object.NumCounters = 1 << 30 }},
{"too many instances", true, func(d *syntheticPerfData) { d.object.NumInstances = 1 << 30 }},
{"counter offset out of range", false, func(d *syntheticPerfData) { d.def.CounterOffset = 1 << 31 }},
{"counter offset out of range with instance", true, func(d *syntheticPerfData) { d.def.CounterOffset = 1 << 20 }},
{"instance name out of range", true, func(d *syntheticPerfData) { d.instance.NameLength = 1 << 31 }},
{"empty counter block", true, func(d *syntheticPerfData) { d.block.ByteLength = 0 }},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
d := newSyntheticPerfData(tc.withInstance)
tc.mutate(d)
buffer := d.bytes(t)
var err error
require.NotPanics(t, func() { _, err = parsePerformanceData(buffer, "") })
require.ErrorIs(t, err, errMalformedPerformanceData)
})
}
}
func TestParsePerformanceDataTruncated(t *testing.T) {
t.Parallel()
for _, withInstance := range []bool{false, true} {
buffer := newSyntheticPerfData(withInstance).bytes(t)
for n := range buffer {
var err error
require.NotPanics(t, func() { _, err = parsePerformanceData(buffer[:n], "") }, "length %d", n)
require.Error(t, err, "length %d", n)
}
}
}
// sharedValuePerfData returns a consistent PERF_DATA_BLOCK with one object
// whose counters all read the same 4 bytes of each minimal counter block.
func sharedValuePerfData(tb testing.TB, numCounters, numInstances uint32) []byte {
tb.Helper()
var body bytes.Buffer
write := func(v any) {
require.NoError(tb, binary.Write(&body, bo, v))
}
for range numCounters {
write(perfCounterDefinition{
ByteLength: uint32(perfCounterDefinitionSize),
CounterType: pdh.PERF_COUNTER_RAWCOUNT,
CounterSize: 4,
})
}
for range numInstances {
write(perfInstanceDefinition{ByteLength: uint32(perfInstanceDefinitionSize)})
write(perfCounterBlock{ByteLength: uint32(perfCounterBlockSize)})
}
object := perfObjectType{
TotalByteLength: uint32(perfObjectTypeSize) + uint32(body.Len()),
DefinitionLength: uint32(perfObjectTypeSize) + numCounters*uint32(perfCounterDefinitionSize),
HeaderLength: uint32(perfObjectTypeSize),
NumCounters: numCounters,
NumInstances: int32(numInstances),
}
header := perfDataBlock{
Signature: [4]uint16{'P', 'E', 'R', 'F'},
LittleEndian: 1,
TotalByteLength: uint32(perfDataBlockSize) + object.TotalByteLength,
HeaderLength: uint32(perfDataBlockSize),
NumObjectTypes: 1,
}
var buf bytes.Buffer
require.NoError(tb, binary.Write(&buf, bo, header))
require.NoError(tb, binary.Write(&buf, bo, object))
buf.Write(body.Bytes())
return buf.Bytes()
}
// TestParsePerformanceDataCounterProduct checks that the counters of all
// instances, which share one allocation, are bounded by the buffer size. Data
// whose counter values overlap could otherwise request an allocation that
// grows with the square of the buffer size.
func TestParsePerformanceDataCounterProduct(t *testing.T) {
t.Parallel()
objects, err := parsePerformanceData(sharedValuePerfData(t, 200, 10), "")
require.NoError(t, err)
require.Len(t, objects[0].Instances, 10)
require.Len(t, objects[0].Instances[9].Counters, 200)
require.Equal(t, perfCounterBlockSize, objects[0].Instances[9].Counters[199].Value)
_, err = parsePerformanceData(sharedValuePerfData(t, 200, 200), "")
require.ErrorIs(t, err, errMalformedPerformanceData)
require.ErrorContains(t, err, "200 instances with 200 counters each")
}
// TestParsePerformanceDataLive parses the data of a real object.
func TestParsePerformanceDataLive(t *testing.T) {
t.Parallel()
buffer := livePerformanceData(t, "Process")
objects, err := parsePerformanceData(buffer, "Process")
require.NoError(t, err)
require.Len(t, objects, 1)
require.NotEmpty(t, objects[0].Instances)
require.NotEmpty(t, objects[0].CounterDefs)
}
// TestParsePerformanceDataNoAlias checks that the parsed objects do not
// reference the buffer, which Collector.Collect reuses for the next query.
func TestParsePerformanceDataNoAlias(t *testing.T) {
t.Parallel()
for _, buffer := range [][]byte{
newSyntheticPerfData(true).bytes(t),
livePerformanceData(t, "Process"),
} {
objects, err := parsePerformanceData(buffer, "")
require.NoError(t, err)
expected, err := parsePerformanceData(bytes.Clone(buffer), "")
require.NoError(t, err)
for i := range buffer {
buffer[i] = 0xFF
}
require.Equal(t, expected, objects)
}
}
func TestQueryRawDataReusesBuffer(t *testing.T) {
t.Parallel()
require.NoError(t, CounterNameTable.load())
query := MapCounterToIndex("Process")
buffer, err := queryRawData(query, nil)
require.NoError(t, err)
reused, err := queryRawData(query, buffer)
require.NoError(t, err)
require.Same(t, unsafe.SliceData(buffer), unsafe.SliceData(reused))
// A buffer below the initial size guess is not used.
small := make([]byte, 16)
buffer, err = queryRawData(query, small)
require.NoError(t, err)
require.NotSame(t, unsafe.SliceData(small), unsafe.SliceData(buffer))
}
func livePerformanceData(tb testing.TB, object string) []byte {
tb.Helper()
require.NoError(tb, CounterNameTable.load())
buffer, err := queryRawData(MapCounterToIndex(object), nil)
require.NoError(tb, err)
return buffer
}
func FuzzParsePerformanceData(f *testing.F) {
f.Add(newSyntheticPerfData(false).bytes(f))
f.Add(newSyntheticPerfData(true).bytes(f))
f.Add(livePerformanceData(f, "System"))
f.Add([]byte{})
f.Fuzz(func(_ *testing.T, data []byte) {
// Must not panic on any input.
_, _ = parsePerformanceData(data, "")
})
}
func TestParseNameTable(t *testing.T) {
t.Parallel()
multiSZ := func(s ...string) []byte {
var buf bytes.Buffer
for _, v := range s {
require.NoError(t, binary.Write(&buf, bo, windows.StringToUTF16(v)))
}
return buf.Bytes()
}
index, names := parseNameTable(multiSZ("1", "1847", "2", "System", "4", "Memory"))
require.Equal(t, map[uint32]string{1: "1847", 2: "System", 4: "Memory"}, index)
require.Equal(t, uint32(4), names["Memory"])
// An incomplete pair and an odd trailing byte are ignored.
buffer := append(multiSZ("2", "System", "4"), 'M')
index, _ = parseNameTable(buffer)
require.Equal(t, map[uint32]string{2: "System"}, index)
index, _ = parseNameTable(nil)
require.Empty(t, index)
}
func TestNewCollectorUnknownObject(t *testing.T) {
t.Parallel()
type counterValues struct {
Name string
ProcessorQueueLength float64 `perfdata:"Processor Queue Length"`
}
_, err := NewCollector[counterValues]("Object That Does Not Exist", nil)
require.ErrorContains(t, err, "not found in the counter name table")
}
// BenchmarkParsePerformanceData measures the parser alone, without the
// RegQueryValueEx call that dominates BenchmarkCollectorCollect.
func BenchmarkParsePerformanceData(b *testing.B) {
buffer := livePerformanceData(b, "Process")
b.ReportAllocs()
for b.Loop() {
if _, err := parsePerformanceData(buffer, "Process"); err != nil {
b.Fatal(err)
}
}
}
// TestDecodeMatchesBinaryRead checks the hand-written decoders of the raw
// structures against binary.Read.
func TestDecodeMatchesBinaryRead(t *testing.T) {
t.Parallel()
type decoder interface {
decode(b []byte) error
}
for _, tc := range []struct {
decoded, expected decoder
}{
{new(perfDataBlock), new(perfDataBlock)},
{new(perfObjectType), new(perfObjectType)},
{new(perfCounterDefinition), new(perfCounterDefinition)},
{new(perfCounterBlock), new(perfCounterBlock)},
{new(perfInstanceDefinition), new(perfInstanceDefinition)},
} {
// Distinct bytes, so a field read from the wrong offset gets a different value.
buffer := make([]byte, binary.Size(tc.expected))
for i := range buffer {
buffer[i] = byte(i + 1)
}
require.NoError(t, binary.Read(bytes.NewReader(buffer), bo, tc.expected))
require.NoError(t, tc.decoded.decode(buffer))
require.Equal(t, tc.expected, tc.decoded)
require.ErrorIs(t, tc.decoded.decode(buffer[:len(buffer)-1]), io.ErrUnexpectedEOF)
}
}
-136
View File
@@ -1,136 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package registry
import (
"sync"
"testing"
)
// TestNewCollectorStructTypeParam guards against a regression where
// reflect.TypeFor[T]().Elem() panicked when T is a plain struct (not a pointer).
// See https://github.com/prometheus-community/windows_exporter/issues/2365
func TestNewCollectorStructTypeParam(t *testing.T) {
type systemCounterValues struct {
Name string
ProcessorQueueLength float64 `perfdata:"Processor Queue Length"`
}
_, err := NewCollector[systemCounterValues]("System", nil)
if err != nil {
t.Skipf("skipping: failed to create collector: %v", err)
}
}
// TestNewCollectorNonStruct guards against a regression where NewCollector
// panicked instead of returning an error for a type parameter that is not a struct.
func TestNewCollectorNonStruct(t *testing.T) {
t.Parallel()
if _, err := NewCollector[int]("System", nil); err == nil {
t.Error("expected an error, got nil")
}
}
func TestCollectNilDestination(t *testing.T) {
t.Parallel()
type counterValues struct {
Name string
}
if err := (&Collector[counterValues]{}).Collect(nil); err == nil {
t.Error("expected an error, got nil")
}
}
// TestCollectConcurrent runs Collect concurrently on one collector, as
// overlapping scrapes do, to let the race detector check the buffer reuse.
func TestCollectConcurrent(t *testing.T) {
t.Parallel()
type systemCounterValues struct {
Name string
ProcessorQueueLength float64 `perfdata:"Processor Queue Length"`
}
collector, err := NewCollector[systemCounterValues]("System", nil)
if err != nil {
t.Skipf("skipping: failed to create collector: %v", err)
}
var wg sync.WaitGroup
for range 8 {
wg.Go(func() {
for range 10 {
var dst []systemCounterValues
if err := collector.Collect(&dst); err != nil {
t.Error(err)
return
}
if len(dst) != 1 {
t.Errorf("expected 1 row, got %d", len(dst))
return
}
}
})
}
wg.Wait()
}
func BenchmarkQueryPerformanceData(b *testing.B) {
for b.Loop() {
_, _ = QueryPerformanceData("Global", "")
}
}
func BenchmarkCollectorCollect(b *testing.B) {
type processCounterValues struct {
Name string
ProcessID float64 `perfdata:"ID Process"`
ProcessorTime float64 `perfdata:"% Processor Time"`
WorkingSet float64 `perfdata:"Working Set"`
HandleCount float64 `perfdata:"Handle Count"`
ThreadCount float64 `perfdata:"Thread Count"`
IOReadBytesSec float64 `perfdata:"IO Read Bytes/sec"`
}
collector, err := NewCollector[processCounterValues]("Process", nil)
if err != nil {
b.Skipf("skipping: failed to create collector: %v", err)
}
var dst []processCounterValues
b.ReportAllocs()
for b.Loop() {
if err := collector.Collect(&dst); err != nil {
b.Fatal(err)
}
}
}
-282
View File
@@ -1,282 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package registry
import (
"io"
"golang.org/x/sys/windows"
)
/*
perfDataBlock
See: https://msdn.microsoft.com/de-de/library/windows/desktop/aa373157(v=vs.85).aspx
typedef struct _PERF_DATA_BLOCK {
WCHAR Signature[4];
DWORD LittleEndian;
DWORD Version;
DWORD Revision;
DWORD TotalByteLength;
DWORD HeaderLength;
DWORD NumObjectTypes;
DWORD DefaultObject;
SYSTEMTIME SystemTime;
LARGE_INTEGER PerfTime;
LARGE_INTEGER PerfFreq;
LARGE_INTEGER PerfTime100nSec;
DWORD SystemNameLength;
DWORD SystemNameOffset;
} PERF_DATA_BLOCK;
*/
type perfDataBlock struct {
Signature [4]uint16
LittleEndian uint32
Version uint32
Revision uint32
TotalByteLength uint32
HeaderLength uint32
NumObjectTypes uint32
DefaultObject int32
SystemTime windows.Systemtime
_ uint32 // unknown field
PerfTime int64
PerfFreq int64
PerfTime100nSec int64
SystemNameLength uint32
SystemNameOffset uint32
}
// decode reads p from the start of b. The fields are decoded directly, since
// binary.Read allocates and walks the struct by reflection on every call.
func (p *perfDataBlock) decode(b []byte) error {
if int64(len(b)) < perfDataBlockSize {
return io.ErrUnexpectedEOF
}
*p = perfDataBlock{
Signature: [4]uint16{bo.Uint16(b[0:]), bo.Uint16(b[2:]), bo.Uint16(b[4:]), bo.Uint16(b[6:])},
LittleEndian: bo.Uint32(b[8:]),
Version: bo.Uint32(b[12:]),
Revision: bo.Uint32(b[16:]),
TotalByteLength: bo.Uint32(b[20:]),
HeaderLength: bo.Uint32(b[24:]),
NumObjectTypes: bo.Uint32(b[28:]),
DefaultObject: int32(bo.Uint32(b[32:])),
SystemTime: windows.Systemtime{
Year: bo.Uint16(b[36:]),
Month: bo.Uint16(b[38:]),
DayOfWeek: bo.Uint16(b[40:]),
Day: bo.Uint16(b[42:]),
Hour: bo.Uint16(b[44:]),
Minute: bo.Uint16(b[46:]),
Second: bo.Uint16(b[48:]),
Milliseconds: bo.Uint16(b[50:]),
},
PerfTime: int64(bo.Uint64(b[56:])),
PerfFreq: int64(bo.Uint64(b[64:])),
PerfTime100nSec: int64(bo.Uint64(b[72:])),
SystemNameLength: bo.Uint32(b[80:]),
SystemNameOffset: bo.Uint32(b[84:]),
}
return nil
}
/*
perfObjectType
See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa373160(v=vs.85).aspx
typedef struct _PERF_OBJECT_TYPE {
DWORD TotalByteLength;
DWORD DefinitionLength;
DWORD HeaderLength;
DWORD ObjectNameTitleIndex;
LPWSTR ObjectNameTitle;
DWORD ObjectHelpTitleIndex;
LPWSTR ObjectHelpTitle;
DWORD DetailLevel;
DWORD NumCounters;
DWORD DefaultCounter;
DWORD NumInstances;
DWORD CodePage;
LARGE_INTEGER PerfTime;
LARGE_INTEGER PerfFreq;
} PERF_OBJECT_TYPE;
*/
type perfObjectType struct {
TotalByteLength uint32
DefinitionLength uint32
HeaderLength uint32
ObjectNameTitleIndex uint32
ObjectNameTitle uint32
ObjectHelpTitleIndex uint32
ObjectHelpTitle uint32
DetailLevel uint32
NumCounters uint32
DefaultCounter int32
NumInstances int32
CodePage uint32
PerfTime int64
PerfFreq int64
}
// decode reads p from the start of b.
func (p *perfObjectType) decode(b []byte) error {
if int64(len(b)) < perfObjectTypeSize {
return io.ErrUnexpectedEOF
}
*p = perfObjectType{
TotalByteLength: bo.Uint32(b[0:]),
DefinitionLength: bo.Uint32(b[4:]),
HeaderLength: bo.Uint32(b[8:]),
ObjectNameTitleIndex: bo.Uint32(b[12:]),
ObjectNameTitle: bo.Uint32(b[16:]),
ObjectHelpTitleIndex: bo.Uint32(b[20:]),
ObjectHelpTitle: bo.Uint32(b[24:]),
DetailLevel: bo.Uint32(b[28:]),
NumCounters: bo.Uint32(b[32:]),
DefaultCounter: int32(bo.Uint32(b[36:])),
NumInstances: int32(bo.Uint32(b[40:])),
CodePage: bo.Uint32(b[44:]),
PerfTime: int64(bo.Uint64(b[48:])),
PerfFreq: int64(bo.Uint64(b[56:])),
}
return nil
}
/*
perfCounterDefinition
See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa373150(v=vs.85).aspx
typedef struct _PERF_COUNTER_DEFINITION {
DWORD ByteLength;
DWORD CounterNameTitleIndex;
LPWSTR CounterNameTitle;
DWORD CounterHelpTitleIndex;
LPWSTR CounterHelpTitle;
LONG DefaultScale;
DWORD DetailLevel;
DWORD CounterType;
DWORD CounterSize;
DWORD CounterOffset;
} PERF_COUNTER_DEFINITION;
*/
type perfCounterDefinition struct {
ByteLength uint32
CounterNameTitleIndex uint32
CounterNameTitle uint32
CounterHelpTitleIndex uint32
CounterHelpTitle uint32
DefaultScale int32
DetailLevel uint32
CounterType uint32
CounterSize uint32
CounterOffset uint32
}
// decode reads p from the start of b.
func (p *perfCounterDefinition) decode(b []byte) error {
if int64(len(b)) < perfCounterDefinitionSize {
return io.ErrUnexpectedEOF
}
*p = perfCounterDefinition{
ByteLength: bo.Uint32(b[0:]),
CounterNameTitleIndex: bo.Uint32(b[4:]),
CounterNameTitle: bo.Uint32(b[8:]),
CounterHelpTitleIndex: bo.Uint32(b[12:]),
CounterHelpTitle: bo.Uint32(b[16:]),
DefaultScale: int32(bo.Uint32(b[20:])),
DetailLevel: bo.Uint32(b[24:]),
CounterType: bo.Uint32(b[28:]),
CounterSize: bo.Uint32(b[32:]),
CounterOffset: bo.Uint32(b[36:]),
}
return nil
}
func (p *perfCounterDefinition) LookupName() string {
return CounterNameTable.LookupString(p.CounterNameTitleIndex)
}
/*
perfCounterBlock
See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa373147(v=vs.85).aspx
typedef struct _PERF_COUNTER_BLOCK {
DWORD ByteLength;
} PERF_COUNTER_BLOCK;
*/
type perfCounterBlock struct {
ByteLength uint32
}
// decode reads p from the start of b.
func (p *perfCounterBlock) decode(b []byte) error {
if int64(len(b)) < perfCounterBlockSize {
return io.ErrUnexpectedEOF
}
p.ByteLength = bo.Uint32(b)
return nil
}
/*
perfInstanceDefinition
See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa373159(v=vs.85).aspx
typedef struct _PERF_INSTANCE_DEFINITION {
DWORD ByteLength;
DWORD ParentObjectTitleIndex;
DWORD ParentObjectInstance;
DWORD UniqueID;
DWORD NameOffset;
DWORD NameLength;
} PERF_INSTANCE_DEFINITION;
*/
type perfInstanceDefinition struct {
ByteLength uint32
ParentObjectTitleIndex uint32
ParentObjectInstance uint32
UniqueID uint32
NameOffset uint32
NameLength uint32
}
// decode reads p from the start of b.
func (p *perfInstanceDefinition) decode(b []byte) error {
if int64(len(b)) < perfInstanceDefinitionSize {
return io.ErrUnexpectedEOF
}
*p = perfInstanceDefinition{
ByteLength: bo.Uint32(b[0:]),
ParentObjectTitleIndex: bo.Uint32(b[4:]),
ParentObjectInstance: bo.Uint32(b[8:]),
UniqueID: bo.Uint32(b[12:]),
NameOffset: bo.Uint32(b[16:]),
NameLength: bo.Uint32(b[20:]),
}
return nil
}
-57
View File
@@ -1,57 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package registry
import (
"io"
"golang.org/x/sys/windows"
)
// decodeUTF16String decodes the unterminated UTF16 string b up to the first NUL.
// A trailing odd byte is ignored. scratch holds the code units; it is returned
// with any growth so the caller can reuse it for the next string. The returned
// string does not reference b.
func decodeUTF16String(b []byte, scratch []uint16) (string, []uint16) {
scratch = scratch[:0]
for i := 0; i+1 < len(b); i += 2 {
scratch = append(scratch, bo.Uint16(b[i:]))
}
return windows.UTF16ToString(scratch), scratch
}
// readUTF16String Reads a null-terminated UTF16 string at the current offset.
func readUTF16String(r io.Reader) (string, error) {
b := make([]byte, 2)
out := make([]uint16, 0, 100)
for {
// A short read at the end of the buffer is an error, not a code unit.
if _, err := io.ReadFull(r, b); err != nil {
return "", err
}
if b[0] == 0 && b[1] == 0 {
return windows.UTF16ToString(out), nil
}
out = append(out, bo.Uint16(b))
}
}
-26
View File
@@ -1,26 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package registry
import (
"strconv"
)
func MapCounterToIndex(name string) string {
return strconv.Itoa(int(CounterNameTable.LookupIndex(name)))
}