pagefile: read the paging file size from the % Usage base (#2722)

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2026-10-15 17:57:30 +00:00
committed by GitHub
parent b90371616b
commit 94b0eff655
7 changed files with 129 additions and 25 deletions
+1
View File
@@ -289,6 +289,7 @@ These metrics keep their names, but 0.31 reported wrong values. Graphs show a st
- `windows_remote_fx_gfx_frames_skipped_insufficient_resource_total` with `resource="client"` and `resource="server"` now count the matching frames. In 0.31 the two label values were swapped.
- `windows_netframework_clrmemory_gc_time_percent` no longer overflows, and it is left out when Windows reports no base value. The `netframework` collector now reads the CLR counters through PDH instead of WMI; names, labels and types are unchanged.
- The IIS URI cache flush and kernel cache item metrics read the right Windows counters, and the metadata cache hit metrics are no longer always `0`.
- `windows_pagefile_limit_bytes` and `windows_pagefile_free_bytes` read the paging file size from the `Paging File` counter instead of the file system. In 0.31 a paging file that could not be read reported a size of `0` and a negative free space; it is now left out.
- `windows_logical_disk_readonly`, the FSRM quota template information, the SMTP general failure totals and the Hyper-V root partition interrupt mappings were documented but never exported. They are exported now.
### performancecounter and PDH-based collectors
+6 -4
View File
@@ -5,7 +5,7 @@ The pagefile collector exposes metrics about the pagefile usage
|||
-|-
Metric name prefix | `pagefile`
Data source | Performance counter `Paging File`, `GetPerformanceInfo`
Data source | Performance counter `Paging File` (`% Usage` and its base), `GetPerformanceInfo` (page size)
Enabled by default? | No
## Flags
@@ -17,16 +17,18 @@ None
| Name | Description | Type | Labels |
|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------|-------|--------|
| `windows_pagefile_free_bytes` | Number of bytes that can be mapped into the operating system paging files without causing any other pages to be swapped out | gauge | `file` |
| `windows_pagefile_limit_bytes` | Number of bytes that can be stored in the operating system paging files. 0 (zero) indicates that there are no paging files | gauge | `file` |
| `windows_pagefile_limit_bytes` | Current size of the paging file in bytes | gauge | `file` |
Both values come from the `% Usage` counter: its raw value is the number of pages in use and its base is the size of
the paging file in pages. They are multiplied by the system page size. A paging file without a valid size is left out.
### Example metric
```
# HELP windows_pagefile_free_bytes OperatingSystem.FreeSpaceInPagingFiles
# HELP windows_pagefile_free_bytes Number of bytes that can be mapped into the operating system paging files without causing any other pages to be swapped out
# TYPE windows_pagefile_free_bytes gauge
windows_pagefile_free_bytes{file="C:\\pagefile.sys"} 6.025797632e+09
# HELP windows_pagefile_limit_bytes OperatingSystem.SizeStoredInPagingFiles
# HELP windows_pagefile_limit_bytes Current size of the paging file in bytes
# TYPE windows_pagefile_limit_bytes gauge
windows_pagefile_limit_bytes{file="C:\\pagefile.sys"} 6.442450944e+09
```
+5 -3
View File
@@ -3,8 +3,10 @@
The collector reads the multi-instance PDH object `Paging File` with
`pdh.CounterTypeRaw`. The object exists on every Windows host with a page
file, so no role is needed. It also uses `GetPerformanceInfo` for the page
size and `os.Stat` for the file size. These notes were recorded on Windows
Server 2025 (build 26100) with one page file.
size. The file size comes from the `% Usage` base (size in pages), not from
`os.Stat`; a file with a base of 0 or a usage outside `[0, base]` is skipped.
These notes were recorded on Windows Server 2025 (build 26100) with one page
file.
- Instances are the page file paths, here `C:\pagefile.sys`, plus `_Total`.
`_Total` is dropped because the collector does not request it. The
@@ -13,7 +15,7 @@ Server 2025 (build 26100) with one page file.
- `% Usage` and `% Usage Peak` are `PERF_RAW_FRACTION`. Despite the `%`, the
raw value is the number of pages in use (a gauge) and the base
(`,secondvalue`) is the page file size in pages. With 4096-byte pages, the
base 622592 matched the file size of 2550136832 bytes from `os.Stat` and
base 622592 matched the file size of 2550136832 bytes on disk and
the 2432 MB `AllocatedBaseSize` of `Win32_PageFileUsage`.
- `% Usage` was 128462 pages in the dump and 0 in a later live sample.
+29 -16
View File
@@ -20,7 +20,6 @@ package pagefile
import (
"fmt"
"log/slog"
"os"
"strings"
"time"
@@ -42,6 +41,7 @@ var ConfigDefaults = Config{}
// A Collector is a Prometheus Collector for WMI metrics.
type Collector struct {
config Config
logger *slog.Logger
perfDataCollector *pdh.Collector[perfDataCounterValues]
perfDataObject []perfDataCounterValues
@@ -77,9 +77,11 @@ func (c *Collector) Close() error {
}
func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
c.logger = logger.With(slog.String("collector", Name))
c.pagingLimitBytes = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "limit_bytes"),
"Number of bytes that can be stored in the operating system paging files. 0 (zero) indicates that there are no paging files",
"Current size of the paging file in bytes",
[]string{"file"},
nil,
)
@@ -93,7 +95,7 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
var err error
c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](logger.With(slog.String("collector", Name)), pdh.CounterTypeRaw, "Paging File", pdh.InstancesAll)
c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](c.logger, pdh.CounterTypeRaw, "Paging File", pdh.InstancesAll)
if err != nil {
return fmt.Errorf("failed to create Paging File collector: %w", err)
}
@@ -114,31 +116,42 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
return err
}
for _, data := range c.perfDataObject {
fileString := strings.ReplaceAll(data.Name, `\??\`, "")
file, err := os.Stat(fileString)
c.collectPagingFiles(ch, c.perfDataObject, float64(gpi.PageSize))
var fileSize float64
return nil
}
// For unknown reasons, Windows doesn't always create a page file. Continue collection rather than aborting.
if err == nil {
fileSize = float64(file.Size())
// collectPagingFiles publishes the size and free space of each paging file.
// Both come from "% Usage": its raw value is the number of pages in use and its
// base is the size of the paging file in pages.
func (c *Collector) collectPagingFiles(ch chan<- prometheus.Metric, data []perfDataCounterValues, pageSize float64) {
for _, file := range data {
// A paging file without a size has no valid sample. Publishing it would
// report a size of 0 and a negative free space.
if file.UsageBase <= 0 || file.Usage < 0 || file.Usage > file.UsageBase {
c.logger.Debug("skipping paging file without a valid size",
slog.String("file", file.Name),
slog.Float64("used_pages", file.Usage),
slog.Float64("size_pages", file.UsageBase),
)
continue
}
fileName := strings.ReplaceAll(file.Name, `\??\`, "")
ch <- prometheus.MustNewConstMetric(
c.pagingFreeBytes,
prometheus.GaugeValue,
fileSize-(data.Usage*float64(gpi.PageSize)),
fileString,
(file.UsageBase-file.Usage)*pageSize,
fileName,
)
ch <- prometheus.MustNewConstMetric(
c.pagingLimitBytes,
prometheus.GaugeValue,
fileSize,
fileString,
file.UsageBase*pageSize,
fileName,
)
}
return nil
}
@@ -0,0 +1,83 @@
// 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 pagefile
import (
"log/slog"
"testing"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
)
type pagingFilesTestCollector struct {
collector *Collector
data []perfDataCounterValues
}
func (p pagingFilesTestCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- p.collector.pagingFreeBytes
ch <- p.collector.pagingLimitBytes
}
func (p pagingFilesTestCollector) Collect(ch chan<- prometheus.Metric) {
p.collector.collectPagingFiles(ch, p.data, 4096)
}
// TestCollectPagingFiles checks that the size and free space come from the
// "% Usage" counter and its base, and that a paging file without a size is
// left out instead of reporting a negative free space.
func TestCollectPagingFiles(t *testing.T) {
t.Parallel()
c := New(nil)
require.NoError(t, c.Build(slog.New(slog.DiscardHandler), nil))
c.perfDataCollector.Close()
registry := prometheus.NewRegistry()
registry.MustRegister(pagingFilesTestCollector{
collector: c,
data: []perfDataCounterValues{
{Name: `\??\C:\pagefile.sys`, Usage: 128462, UsageBase: 622592},
{Name: `D:\pagefile.sys`, Usage: 0, UsageBase: 0},
{Name: `E:\pagefile.sys`, Usage: 10, UsageBase: 5},
},
})
families, err := registry.Gather()
require.NoError(t, err)
values := make(map[string]map[string]float64)
for _, family := range families {
require.Equal(t, "GAUGE", family.GetType().String(), family.GetName())
values[family.GetName()] = make(map[string]float64)
for _, metric := range family.GetMetric() {
require.Len(t, metric.GetLabel(), 1)
values[family.GetName()][metric.GetLabel()[0].GetValue()] = metric.GetGauge().GetValue()
}
}
require.Equal(t, map[string]map[string]float64{
"windows_pagefile_limit_bytes": {`C:\pagefile.sys`: 622592 * 4096},
"windows_pagefile_free_bytes": {`C:\pagefile.sys`: (622592 - 128462) * 4096},
}, values)
}
+4 -1
View File
@@ -20,5 +20,8 @@ package pagefile
type perfDataCounterValues struct {
Name string
Usage float64 `perfdata:"% Usage"`
// "% Usage" is PERF_RAW_FRACTION: the raw value is the number of pages
// in use, the base is the size of the paging file in pages.
Usage float64 `perfdata:"% Usage"`
UsageBase float64 `perfdata:"% Usage,secondvalue"`
}
+1 -1
View File
@@ -287,7 +287,7 @@ windows_exporter_collector_timeout{collector="udp"} 0
# TYPE windows_os_install_time_timestamp_seconds gauge
# HELP windows_pagefile_free_bytes Number of bytes that can be mapped into the operating system paging files without causing any other pages to be swapped out
# TYPE windows_pagefile_free_bytes gauge
# HELP windows_pagefile_limit_bytes Number of bytes that can be stored in the operating system paging files. 0 (zero) indicates that there are no paging files
# HELP windows_pagefile_limit_bytes Current size of the paging file in bytes
# TYPE windows_pagefile_limit_bytes gauge
# HELP windows_performancecounter_collector_duration_seconds windows_exporter: Duration of an performancecounter child collection.
# TYPE windows_performancecounter_collector_duration_seconds gauge