diff --git a/UPGRADE.md b/UPGRADE.md index 240c65a1..899e61ca 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -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 diff --git a/docs/collector.pagefile.md b/docs/collector.pagefile.md index bc9b7410..0eb3d448 100644 --- a/docs/collector.pagefile.md +++ b/docs/collector.pagefile.md @@ -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 ``` diff --git a/internal/collector/pagefile/AGENTS.md b/internal/collector/pagefile/AGENTS.md index 27f75bf8..54b3dbdc 100644 --- a/internal/collector/pagefile/AGENTS.md +++ b/internal/collector/pagefile/AGENTS.md @@ -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. diff --git a/internal/collector/pagefile/pagefile.go b/internal/collector/pagefile/pagefile.go index b138f1f2..a9c5ea22 100644 --- a/internal/collector/pagefile/pagefile.go +++ b/internal/collector/pagefile/pagefile.go @@ -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 } diff --git a/internal/collector/pagefile/pagefile_internal_test.go b/internal/collector/pagefile/pagefile_internal_test.go new file mode 100644 index 00000000..9460983e --- /dev/null +++ b/internal/collector/pagefile/pagefile_internal_test.go @@ -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) +} diff --git a/internal/collector/pagefile/types.go b/internal/collector/pagefile/types.go index 25512aeb..8433f81e 100644 --- a/internal/collector/pagefile/types.go +++ b/internal/collector/pagefile/types.go @@ -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"` } diff --git a/tools/e2e-output.txt b/tools/e2e-output.txt index 89d025c8..c53cc9c9 100644 --- a/tools/e2e-output.txt +++ b/tools/e2e-output.txt @@ -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