From 27186f7e7862e02af292f2062a6bf71a0ae27e84 Mon Sep 17 00:00:00 2001 From: Dominik Eisenberg <64131471+Dominik-esb@users.noreply.github.com> Date: Mon, 29 Dec 2025 20:23:02 +0100 Subject: [PATCH] os: add system installation date to metrics (#2284) Co-authored-by: EisenbergD --- docs/collector.os.md | 14 +++++++----- internal/collector/os/os.go | 43 +++++++++++++++++++++++++++++++++++++ tools/e2e-output.txt | 2 ++ 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/docs/collector.os.md b/docs/collector.os.md index 39005336..c5b96b1b 100644 --- a/docs/collector.os.md +++ b/docs/collector.os.md @@ -14,10 +14,11 @@ None ## Metrics -| Name | Description | Type | Labels | -|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|-------|------------------------------------------------------------------------| -| `windows_os_hostname` | Labelled system hostname information as provided by ComputerSystem.DNSHostName and ComputerSystem.Domain | gauge | `domain`, `fqdn`, `hostname` | -| `windows_os_info` | Contains full product name & version in labels. Note that the `major_version` for Windows 11 is "10"; a build number greater than 22000 represents Windows 11. | gauge | `product`, `version`, `major_version`, `minor_version`, `build_number`, `installation_type`| +| Name | Description | Type | Labels | +|--------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|-------|-----------------------------------------------------------------------------------------------------------------| +| `windows_os_hostname` | Labelled system hostname information as provided by ComputerSystem.DNSHostName and ComputerSystem.Domain | gauge | `domain`, `fqdn`, `hostname` | +| `windows_os_info` | Contains full product name & version in labels. Note that the `major_version` for Windows 11 is "10"; a build number greater than 22000 represents Windows 11. | gauge | `product`, `version`, `major_version`, `minor_version`, `build_number`, `revision`, `installation_type` | +| `windows_os_install_time_timestamp` | Unix timestamp of OS installation time | gauge | None | ### Example metric @@ -27,7 +28,10 @@ None windows_os_hostname{domain="",fqdn="PC",hostname="PC"} 1 # HELP windows_os_info Contains full product name & version in labels. Note that the "major_version" for Windows 11 is \\"10\\"; a build number greater than 22000 represents Windows 11. # TYPE windows_os_info gauge -windows_os_info{build_number="19045",major_version="10",minor_version="0",product="Windows 10 Pro",revision="4842",version="10.0.19045"} 1 +windows_os_info{build_number="19045",installation_type="Client",major_version="10",minor_version="0",product="Windows 10 Pro",revision="4842",version="10.0.19045"} 1 +# HELP windows_os_install_time_timestamp Unix timestamp of OS installation time +# TYPE windows_os_install_time_timestamp gauge +windows_os_install_time_timestamp 1.6725312e+09 ``` ## Useful queries diff --git a/internal/collector/os/os.go b/internal/collector/os/os.go index daf4dd72..08cde8de 100644 --- a/internal/collector/os/os.go +++ b/internal/collector/os/os.go @@ -44,8 +44,11 @@ var ConfigDefaults = Config{} type Collector struct { config Config + installTimeTimestamp float64 + hostname *prometheus.Desc osInformation *prometheus.Desc + installTime *prometheus.Desc } func New(config *Config) *Collector { @@ -78,6 +81,13 @@ func (c *Collector) Build(_ *slog.Logger, _ *mi.Session) error { return fmt.Errorf("failed to get Windows version: %w", err) } + installTimeTimestamp, err := c.getInstallTime() + if err != nil { + return fmt.Errorf("failed to get install time: %w", err) + } + + c.installTimeTimestamp = installTimeTimestamp + version := osversion.Get() // Microsoft has decided to keep the major version as "10" for Windows 11, including the product name. @@ -111,6 +121,13 @@ func (c *Collector) Build(_ *slog.Logger, _ *mi.Session) error { nil, ) + c.installTime = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "install_time_timestamp"), + "Unix timestamp of OS installation time", + nil, + nil, + ) + return nil } @@ -125,6 +142,12 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric) error { 1.0, ) + ch <- prometheus.MustNewConstMetric( + c.installTime, + prometheus.GaugeValue, + c.installTimeTimestamp, + ) + if err := c.collectHostname(ch); err != nil { errs = append(errs, fmt.Errorf("failed to collect hostname metrics: %w", err)) } @@ -190,3 +213,23 @@ func (c *Collector) getWindowsVersion() (string, string, string, error) { return strings.TrimSpace(productName), strconv.FormatUint(revision, 10), strings.TrimSpace(installationType), nil } + +func (c *Collector) getInstallTime() (float64, error) { + ntKey, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) + if err != nil { + return 0, fmt.Errorf("failed to open registry key: %w", err) + } + + defer func(ntKey registry.Key) { + _ = ntKey.Close() + }(ntKey) + + installDate, _, err := ntKey.GetIntegerValue("InstallDate") + if errors.Is(err, registry.ErrNotExist) { + return 0, nil + } else if err != nil { + return 0, err + } + + return float64(installDate), nil +} diff --git a/tools/e2e-output.txt b/tools/e2e-output.txt index 33991fca..bce795fa 100644 --- a/tools/e2e-output.txt +++ b/tools/e2e-output.txt @@ -287,6 +287,8 @@ windows_exporter_collector_timeout{collector="udp"} 0 # TYPE windows_os_hostname gauge # HELP windows_os_info Contains full product name & version in labels. Note that the "major_version" for Windows 11 is \\"10\\"; a build number greater than 22000 represents Windows 11. # TYPE windows_os_info gauge +# HELP windows_os_install_time_timestamp Unix timestamp of OS installation time +# TYPE windows_os_install_time_timestamp 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