mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-19 11:09:03 +02:00
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:
co-authored by
Claude Opus 5.5
parent
4ca7dc8957
commit
651daf0fee
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user