mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-20 19:49: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
@@ -16,8 +16,8 @@ their runtime behavior.
|
|||||||
- [`internal/collector`](internal/collector): built-in collectors. Each implements
|
- [`internal/collector`](internal/collector): built-in collectors. Each implements
|
||||||
`GetName`, `Build`, `Collect` and `Close` from
|
`GetName`, `Build`, `Collect` and `Close` from
|
||||||
[`pkg/collector/types.go`](pkg/collector/types.go).
|
[`pkg/collector/types.go`](pkg/collector/types.go).
|
||||||
- [`internal/pdh`](internal/pdh) and [`internal/pdh/registry`](internal/pdh/registry):
|
- [`internal/pdh`](internal/pdh):
|
||||||
performance counter queries and perflib decoding. [`internal/mi`](internal/mi)
|
performance counter queries. [`internal/mi`](internal/mi)
|
||||||
provides Management Infrastructure queries; [`internal/ole`](internal/ole) and
|
provides Management Infrastructure queries; [`internal/ole`](internal/ole) and
|
||||||
[`internal/headers`](internal/headers) contain native Windows bindings.
|
[`internal/headers`](internal/headers) contain native Windows bindings.
|
||||||
- [`internal/config`](internal/config), [`internal/httphandler`](internal/httphandler)
|
- [`internal/config`](internal/config), [`internal/httphandler`](internal/httphandler)
|
||||||
@@ -86,7 +86,7 @@ exercised a live Windows role.
|
|||||||
even when the Windows counter name says average, percentage or seconds.
|
even when the Windows counter name says average, percentage or seconds.
|
||||||
Convert units explicitly; expose the base/operation count needed for ratios.
|
Convert units explicitly; expose the base/operation count needed for ratios.
|
||||||
Never publish an invalid sample as zero: that can look like a counter reset.
|
Never publish an invalid sample as zero: that can look like a counter reset.
|
||||||
- Keep the typed PDH/registry collectors and `perfdata` tags. Preserve duplicate
|
- Keep the typed PDH collectors and `perfdata` tags. Preserve duplicate
|
||||||
instance occurrences within a sample; their suffixes are not stable identities.
|
instance occurrences within a sample; their suffixes are not stable identities.
|
||||||
Do not remove every name ending in `_Total`: legitimate process and database
|
Do not remove every name ending in `_Total`: legitimate process and database
|
||||||
names can use that suffix. Use the shared aggregate-instance policy.
|
names can use that suffix. Use the shared aggregate-instance policy.
|
||||||
|
|||||||
+16
-21
@@ -2,20 +2,21 @@
|
|||||||
|
|
||||||
The process collector exposes metrics about processes.
|
The process collector exposes metrics about processes.
|
||||||
|
|
||||||
Note, on Windows Server 2022, the `Process` counter set is disabled by default. To enable it, run the following command in an elevated PowerShell session:
|
The collector reads all processes with one `NtQuerySystemInformation(SystemProcessInformation)` call.
|
||||||
|
The perflib `Process` counter set reads the same data, so the values and process names are the same.
|
||||||
|
The collector does not use performance counters. It works when the `Process` counter set is disabled,
|
||||||
|
for example on Windows Server 2022, and it does not require administrator rights.
|
||||||
|
|
||||||
```powershell
|
The `owner`, `cmdline` and `process_group_id` labels of `windows_process_info` require opening the process.
|
||||||
lodctr.exe /E:Lsa
|
Without administrator rights, they are empty for processes of other users and for protected processes.
|
||||||
lodctr.exe /E:PerfProc
|
The collector reads them once per process and caches them while the process is running and matches the filters.
|
||||||
lodctr.exe /R
|
A failed read is retried on the next scrape.
|
||||||
```
|
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---------------------|-----------|
|
|---------------------|----------------------------|
|
||||||
| Metric name prefix | `process` |
|
| Metric name prefix | `process` |
|
||||||
| Data source | Perflib |
|
| Data source | `NtQuerySystemInformation` |
|
||||||
| Counters | `Process` |
|
| Enabled by default? | No |
|
||||||
| Enabled by default? | No |
|
|
||||||
|
|
||||||
## Flags
|
## Flags
|
||||||
|
|
||||||
@@ -38,12 +39,6 @@ See [IIS worker processes](#iis-worker-processes).
|
|||||||
|
|
||||||
Disabled by default, and can be enabled with `--collector.process.iis`. NOTE: Just plain parameter without `true`.
|
Disabled by default, and can be enabled with `--collector.process.iis`. NOTE: Just plain parameter without `true`.
|
||||||
|
|
||||||
### `--collector.process.counter-version`
|
|
||||||
|
|
||||||
Version of the process collector to use. 1 for Process V1, 2 for Process V2.
|
|
||||||
0 uses Process V2 if it is available and falls back to Process V1 otherwise.
|
|
||||||
Defaults to 1 (Process V1).
|
|
||||||
|
|
||||||
### `--collector.process.cmdline`
|
### `--collector.process.cmdline`
|
||||||
|
|
||||||
Enables the `cmdline` label for the process metrics.
|
Enables the `cmdline` label for the process metrics.
|
||||||
@@ -52,9 +47,9 @@ Enabled by default, and can be turned off with `--no-collector.process.cmdline`.
|
|||||||
|
|
||||||
### Example
|
### Example
|
||||||
To match all firefox processes: `--collector.process.include="firefox.*"`.
|
To match all firefox processes: `--collector.process.include="firefox.*"`.
|
||||||
Note that multiple processes with the same name will be disambiguated by
|
The process name is the image name without the `.exe` extension, like the instance names of the `Process` counter set.
|
||||||
Windows by adding a number suffix, such as `firefox#2`. Your [regular expression](https://en.wikipedia.org/wiki/Regular_expression) must take
|
Processes with the same name have the same `process` label and differ in the `process_id` label.
|
||||||
these suffixes into consideration.
|
A `#` in the image name is kept: `app#2.exe` is `app#2`. Earlier versions cut the name at the first `#`.
|
||||||
|
|
||||||
:warning: The regular expression is case-sensitive, so `--collector.process.include="FIREFOX.*"` will **NOT** match a process named `firefox` .
|
:warning: The regular expression is case-sensitive, so `--collector.process.include="FIREFOX.*"` will **NOT** match a process named `firefox` .
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
IIS application pool names (`--collector.process.iis`) come from two sources.
|
||||||
|
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ import (
|
|||||||
"golang.org/x/sys/windows"
|
"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"
|
const workerProcessName = "w3wp"
|
||||||
|
|
||||||
// errNoAppPoolArgument reports a w3wp command line without a non-empty -ap argument.
|
// errNoAppPoolArgument reports a w3wp command line without a non-empty -ap argument.
|
||||||
var errNoAppPoolArgument = errors.New("command line has no -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 {
|
func isWorkerProcess(name string) bool {
|
||||||
return strings.EqualFold(name, workerProcessName)
|
return strings.EqualFold(name, workerProcessName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -370,7 +370,6 @@ func TestBuildCloseWorkerProcess(t *testing.T) {
|
|||||||
ProcessInclude: regexp.MustCompile(`^(?:w3wp)$`),
|
ProcessInclude: regexp.MustCompile(`^(?:w3wp)$`),
|
||||||
ProcessExclude: types.RegExpEmpty,
|
ProcessExclude: types.RegExpEmpty,
|
||||||
EnableWorkerProcess: true,
|
EnableWorkerProcess: true,
|
||||||
CounterVersion: 1,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
logger := slog.New(slog.DiscardHandler)
|
logger := slog.New(slog.DiscardHandler)
|
||||||
|
|||||||
@@ -25,16 +25,11 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/alecthomas/kingpin/v2"
|
"github.com/alecthomas/kingpin/v2"
|
||||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
"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-community/windows_exporter/internal/types"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
@@ -42,12 +37,21 @@ import (
|
|||||||
|
|
||||||
const Name = "process"
|
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 {
|
type Config struct {
|
||||||
ProcessInclude *regexp.Regexp `yaml:"include"`
|
ProcessInclude *regexp.Regexp `yaml:"include"`
|
||||||
ProcessExclude *regexp.Regexp `yaml:"exclude"`
|
ProcessExclude *regexp.Regexp `yaml:"exclude"`
|
||||||
EnableWorkerProcess bool `yaml:"iis"`
|
EnableWorkerProcess bool `yaml:"iis"`
|
||||||
EnableCMDLine bool `yaml:"cmdline"`
|
EnableCMDLine bool `yaml:"cmdline"`
|
||||||
CounterVersion uint8 `yaml:"counter-version"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gochecknoglobals
|
//nolint:gochecknoglobals
|
||||||
@@ -56,7 +60,6 @@ var ConfigDefaults = Config{
|
|||||||
ProcessExclude: types.RegExpEmpty,
|
ProcessExclude: types.RegExpEmpty,
|
||||||
EnableWorkerProcess: false,
|
EnableWorkerProcess: false,
|
||||||
EnableCMDLine: true,
|
EnableCMDLine: true,
|
||||||
CounterVersion: 1,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Collector struct {
|
type Collector struct {
|
||||||
@@ -69,10 +72,10 @@ type Collector struct {
|
|||||||
miSession *mi.Session
|
miSession *mi.Session
|
||||||
workerProcessMIQueryQuery mi.Query
|
workerProcessMIQueryQuery mi.Query
|
||||||
|
|
||||||
perfDataCollector pdhtypes.Collector[perfDataCounterValues]
|
snapshot snapshotBuffer
|
||||||
perfDataObject []perfDataCounterValues
|
processes []processSnapshot
|
||||||
workerCh chan processWorkerRequest
|
// infoCache holds the windows_process_info values of the processes of the last scrape, keyed by PID.
|
||||||
workerWG sync.WaitGroup
|
infoCache map[uint32]processInfo
|
||||||
|
|
||||||
lookupCache sync.Map
|
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.",
|
"If enabled, the full cmdline is exposed to the windows_process_info metrics.",
|
||||||
).Default(strconv.FormatBool(c.config.EnableCMDLine)).BoolVar(&c.config.EnableCMDLine)
|
).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 {
|
app.Action(func(*kingpin.ParseContext) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -175,15 +173,9 @@ func (c *Collector) Close() error {
|
|||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
if c.perfDataCollector != nil {
|
c.snapshot = snapshotBuffer{}
|
||||||
c.perfDataCollector.Close()
|
c.processes = nil
|
||||||
}
|
c.infoCache = nil
|
||||||
|
|
||||||
if c.workerCh != nil {
|
|
||||||
close(c.workerCh)
|
|
||||||
c.workerCh = nil
|
|
||||||
c.workerWG.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// The session belongs to the caller of Build.
|
// The session belongs to the caller of Build.
|
||||||
c.miSession = nil
|
c.miSession = nil
|
||||||
@@ -194,40 +186,7 @@ func (c *Collector) Close() error {
|
|||||||
func (c *Collector) Build(logger *slog.Logger, miSession *mi.Session) error {
|
func (c *Collector) Build(logger *slog.Logger, miSession *mi.Session) error {
|
||||||
c.logger = logger.With(slog.String("collector", Name))
|
c.logger = logger.With(slog.String("collector", Name))
|
||||||
|
|
||||||
var err error
|
c.infoCache = nil
|
||||||
|
|
||||||
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.lookupCache = sync.Map{}
|
c.lookupCache = sync.Map{}
|
||||||
|
|
||||||
// The flags wrap the expressions in ^(?:...)$, the defaults of [New] are [types.RegExpAny] and [types.RegExpEmpty].
|
// 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 {
|
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
|
// workerProcessWMIQuery returns the root\WebAdministration query, or nil if it was unavailable during Build.
|
||||||
func (c *Collector) getProcessInformation(pid uint32) (string, string, uint32, error) {
|
func (c *Collector) workerProcessWMIQuery(maxScrapeDuration time.Duration) func() ([]WorkerProcess, error) {
|
||||||
if pid == 0 {
|
miSession, miQuery := c.miSession, c.workerProcessMIQueryQuery
|
||||||
return "", "", 0, nil
|
if miSession == nil {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
hProcess, vmReadAccess, err := c.openProcess(pid)
|
return func() ([]WorkerProcess, error) {
|
||||||
if err != nil {
|
var workerProcesses []WorkerProcess
|
||||||
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
|
||||||
return "", "", 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
// resolveAppPools maps IIS worker process IDs to application pool names.
|
||||||
// Get the process environment block (PEB) address
|
//
|
||||||
var pbi windows.PROCESS_BASIC_INFORMATION
|
// 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))
|
var unresolved []uint32
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
peb := windows.PEB{}
|
for _, pid := range pids {
|
||||||
|
appPool, err := readCommandLine(pid)
|
||||||
|
if err == nil {
|
||||||
|
appPools[pid] = appPool
|
||||||
|
|
||||||
err := windows.ReadProcessMemory(hProcess,
|
continue
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
processParameters := windows.RTL_USER_PROCESS_PARAMETERS{}
|
logger.LogAttrs(context.Background(), slog.LevelDebug, "Failed to read the IIS application pool from the worker process command line",
|
||||||
|
slog.Uint64("pid", uint64(pid)),
|
||||||
err = windows.ReadProcessMemory(hProcess,
|
slog.Any("err", err),
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
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
|
if len(unresolved) == 0 || queryWMI == nil {
|
||||||
}
|
return appPools, 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) {
|
workerProcesses, err := queryWMI()
|
||||||
if err := tok.Close(); err != nil {
|
|
||||||
logger.Warn("Token close failed",
|
|
||||||
slog.Any("err", err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}(tok)
|
|
||||||
|
|
||||||
tokenUser, err := tok.GetTokenUser()
|
|
||||||
if err != nil {
|
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()
|
byPID := make(map[uint64]string, len(workerProcesses))
|
||||||
|
for _, wp := range workerProcesses {
|
||||||
var owner string
|
if _, ok := byPID[wp.ProcessId]; !ok && wp.AppPoolName != "" {
|
||||||
|
byPID[wp.ProcessId] = wp.AppPoolName
|
||||||
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
|
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) {
|
func (c *Collector) collectProcess(ch chan<- prometheus.Metric, name string, process *processSnapshot, info *processInfo) {
|
||||||
// Open the process with QUERY_INFORMATION and VM_READ permissions.
|
pid := strconv.FormatUint(uint64(process.pid), 10)
|
||||||
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_ACCESS_DENIED) {
|
ch <- prometheus.MustNewConstMetric(
|
||||||
return 0, false, fmt.Errorf("failed to open process: %w", err)
|
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
|
// Perflib reports whole seconds for the Elapsed Time counter of the Process counter set.
|
||||||
return 0, false, errors.New("process not found")
|
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)
|
ch <- prometheus.MustNewConstMetric(
|
||||||
if err != nil {
|
c.handleCount,
|
||||||
return 0, false, fmt.Errorf("failed to open process with limited permissions: %w", err)
|
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)$`),
|
ProcessInclude: regexp.MustCompile(`^(?:w3wp)$`),
|
||||||
ProcessExclude: types.RegExpEmpty,
|
ProcessExclude: types.RegExpEmpty,
|
||||||
EnableWorkerProcess: true,
|
EnableWorkerProcess: true,
|
||||||
CounterVersion: 1,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
require.Contains(t, families, "windows_process_info")
|
require.Contains(t, families, "windows_process_info")
|
||||||
@@ -142,12 +141,10 @@ func TestCollectorWorkerProcessCommandLine(t *testing.T) {
|
|||||||
ProcessInclude: regexp.MustCompile(`^(?:w3wp)$`),
|
ProcessInclude: regexp.MustCompile(`^(?:w3wp)$`),
|
||||||
ProcessExclude: types.RegExpEmpty,
|
ProcessExclude: types.RegExpEmpty,
|
||||||
EnableWorkerProcess: true,
|
EnableWorkerProcess: true,
|
||||||
CounterVersion: 1,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if _, ok := families["windows_process_info"]; !ok {
|
// The process snapshot includes suspended processes.
|
||||||
t.Skip("the Process counter set has no w3wp instance")
|
require.Contains(t, families, "windows_process_info", "no w3wp process in the process snapshot")
|
||||||
}
|
|
||||||
|
|
||||||
for _, metric := range families["windows_process_info"].GetMetric() {
|
for _, metric := range families["windows_process_info"].GetMetric() {
|
||||||
labels := make(map[string]string)
|
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
|
package process_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/alecthomas/kingpin/v2"
|
"github.com/alecthomas/kingpin/v2"
|
||||||
"github.com/prometheus-community/windows_exporter/internal/collector/process"
|
"github.com/prometheus-community/windows_exporter/internal/collector/process"
|
||||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkProcessCollector(b *testing.B) {
|
func BenchmarkProcessCollector(b *testing.B) {
|
||||||
@@ -37,3 +41,40 @@ func BenchmarkProcessCollector(b *testing.B) {
|
|||||||
func TestCollector(t *testing.T) {
|
func TestCollector(t *testing.T) {
|
||||||
testutils.TestCollector(t, process.New, nil)
|
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"`
|
AppPoolName string `mi:"AppPoolName"`
|
||||||
ProcessId uint64 `mi:"ProcessId"`
|
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"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus-community/windows_exporter/internal/pdh"
|
"github.com/prometheus-community/windows_exporter/internal/pdh"
|
||||||
"github.com/prometheus-community/windows_exporter/internal/pdh/registry"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -78,30 +77,22 @@ func TestTotalSuffixProcess(t *testing.T) {
|
|||||||
ID float64 `perfdata:"ID Process"`
|
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)
|
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.Eventually(t, func() bool {
|
||||||
require.NoError(t, err)
|
var rows []processValues
|
||||||
t.Cleanup(modern.Close)
|
if collector.Collect(&rows) != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
for name, collect := range map[string]func(*[]processValues) error{"registry": legacy.Collect, "explicit PDH": modern.Collect} {
|
for _, row := range rows {
|
||||||
t.Run(name, func(t *testing.T) {
|
if row.ID == float64(child.Process.Pid) && row.Name == "worker_Total" {
|
||||||
require.Eventually(t, func() bool {
|
return true
|
||||||
var rows []processValues
|
}
|
||||||
if collect(&rows) != nil {
|
}
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, row := range rows {
|
return false
|
||||||
if row.ID == float64(child.Process.Pid) && row.Name == "worker_Total" {
|
}, time.Second*5, time.Millisecond*100)
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}, time.Second*5, time.Millisecond*100)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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() {}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)))
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user