From 298d820bd63ca51d55273b65ec721ac768700691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Fri, 30 May 2025 19:52:27 +0200 Subject: [PATCH] time: expose clock source sync (#2058) --- docs/collector.time.md | 16 ++++++- internal/collector/time/time.go | 80 ++++++++++++++++++++++++++++----- pkg/collector/collect.go | 28 +++++++----- tools/e2e-output.txt | 12 ++--- tools/end-to-end-test.ps1 | 2 +- 5 files changed, 106 insertions(+), 32 deletions(-) diff --git a/docs/collector.time.md b/docs/collector.time.md index da1873da..6754169f 100644 --- a/docs/collector.time.md +++ b/docs/collector.time.md @@ -1,6 +1,6 @@ # time collector -The time collector exposes the Windows Time Service metrics. Note that the Windows Time Service must be running, else metric collection will fail. +The time collector exposes the Windows Time Service and other time related metrics. If the Windows Time Service is stopped after collection has started, collector metric values will reset to 0. Please note the Time Service perflib counters are only available on [Windows Server 2016 or newer](https://docs.microsoft.com/en-us/windows-server/networking/windows-time-service/windows-server-2016-improvements). @@ -32,9 +32,21 @@ Matching is case-sensitive. | `windows_time_ntp_server_incoming_requests_total` | Total number of requests received by the NTP server. | counter | None | | `windows_time_current_timestamp_seconds` | Current time as reported by the operating system, in [Unix time](https://en.wikipedia.org/wiki/Unix_time). See [time.Unix()](https://golang.org/pkg/time/#Unix) for details | gauge | None | | `windows_time_timezone` | Current timezone as reported by the operating system. | gauge | `timezone` | +| `windows_time_clock_sync_source` | This value reflects the sync source of the system clock. | gauge | `type` | ### Example metric -_This collector does not yet have explained examples, we would appreciate your help adding them!_ +``` +# HELP windows_time_clock_sync_source This value reflects the sync source of the system clock. +# TYPE windows_time_clock_sync_source gauge +windows_time_clock_sync_source{type="AllSync"} 0 +windows_time_clock_sync_source{type="Local CMOS Clock"} 0 +windows_time_clock_sync_source{type="NT5DS"} 0 +windows_time_clock_sync_source{type="NTP"} 1 +windows_time_clock_sync_source{type="NoSync"} 0 +# HELP windows_time_current_timestamp_seconds OperatingSystem.LocalDateTime +# TYPE windows_time_current_timestamp_seconds gauge +windows_time_current_timestamp_seconds 1.74862554e+09 +``` ## Useful queries _This collector does not yet have any useful queries added, we would appreciate your help adding them!_ diff --git a/internal/collector/time/time.go b/internal/collector/time/time.go index 8e2d77fa..df0ccdfc 100644 --- a/internal/collector/time/time.go +++ b/internal/collector/time/time.go @@ -33,13 +33,15 @@ import ( "github.com/prometheus-community/windows_exporter/internal/types" "github.com/prometheus/client_golang/prometheus" "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" ) const ( Name = "time" - collectorSystemTime = "system_time" - collectorNTP = "ntp" + collectorSystemTime = "system_time" + collectorClockSource = "clock_source" + collectorNTP = "ntp" ) type Config struct { @@ -50,6 +52,7 @@ type Config struct { var ConfigDefaults = Config{ CollectorsEnabled: []string{ collectorSystemTime, + collectorClockSource, collectorNTP, }, } @@ -61,10 +64,13 @@ type Collector struct { perfDataCollector *pdh.Collector perfDataObject []perfDataCounterValues + logger *slog.Logger + ppbCounterPresent bool currentTime *prometheus.Desc timezone *prometheus.Desc + clockSource *prometheus.Desc clockFrequencyAdjustment *prometheus.Desc clockFrequencyAdjustmentPPB *prometheus.Desc computedTimeOffset *prometheus.Desc @@ -124,9 +130,11 @@ func (c *Collector) Close() error { return nil } -func (c *Collector) Build(_ *slog.Logger, _ *mi.Session) error { +func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error { + c.logger = logger.With(slog.String("collector", Name)) + for _, collector := range c.config.CollectorsEnabled { - if !slices.Contains([]string{collectorSystemTime, collectorNTP}, collector) { + if !slices.Contains([]string{collectorSystemTime, collectorClockSource, collectorNTP}, collector) { return fmt.Errorf("unknown collector: %s", collector) } } @@ -136,16 +144,22 @@ func (c *Collector) Build(_ *slog.Logger, _ *mi.Session) error { c.currentTime = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "current_timestamp_seconds"), - "OperatingSystem.LocalDateTime", + "Current time as reported by the operating system, in unix time.", nil, nil, ) c.timezone = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "timezone"), - "OperatingSystem.LocalDateTime", + "Current timezone as reported by the operating system.", []string{"timezone"}, nil, ) + c.clockSource = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "clock_sync_source"), + "This value reflects the sync source of the system clock.", + []string{"type"}, + nil, + ) c.clockFrequencyAdjustment = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "clock_frequency_adjustment"), "This value reflects the adjustment made to the local system clock frequency by W32Time in nominal clock units. This counter helps visualize the finer adjustments being made by W32time to synchronize the local clock.", @@ -189,11 +203,13 @@ func (c *Collector) Build(_ *slog.Logger, _ *mi.Session) error { nil, ) - var err error + if slices.Contains(c.config.CollectorsEnabled, collectorNTP) { + var err error - c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](pdh.CounterTypeRaw, "Windows Time Service", nil) - if err != nil { - return fmt.Errorf("failed to create Windows Time Service collector: %w", err) + c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](pdh.CounterTypeRaw, "Windows Time Service", nil) + if err != nil { + return fmt.Errorf("failed to create Windows Time Service collector: %w", err) + } } return nil @@ -206,7 +222,13 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric) error { if slices.Contains(c.config.CollectorsEnabled, collectorSystemTime) { if err := c.collectTime(ch); err != nil { - errs = append(errs, fmt.Errorf("failed collecting time metrics: %w", err)) + errs = append(errs, fmt.Errorf("failed collecting operating system time metrics: %w", err)) + } + } + + if slices.Contains(c.config.CollectorsEnabled, collectorClockSource) { + if err := c.collectClockSource(ch); err != nil { + errs = append(errs, fmt.Errorf("failed collecting clock source metrics: %w", err)) } } @@ -244,6 +266,42 @@ func (c *Collector) collectTime(ch chan<- prometheus.Metric) error { return nil } +func (c *Collector) collectClockSource(ch chan<- prometheus.Metric) error { + keyPath := `SYSTEM\CurrentControlSet\Services\W32Time\Parameters` + + key, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.READ) + if err != nil { + return fmt.Errorf("failed to open registry key: %w", err) + } + + val, _, err := key.GetStringValue("Type") + if err != nil { + return fmt.Errorf("failed to read 'Type' value: %w", err) + } + + for _, validType := range []string{"NTP", "NT5DS", "AllSync", "NoSync", "Local CMOS Clock"} { + metricValue := 0.0 + if val == validType { + metricValue = 1.0 + } + + ch <- prometheus.MustNewConstMetric( + c.clockSource, + prometheus.GaugeValue, + metricValue, + validType, + ) + } + + if err := key.Close(); err != nil { + c.logger.Debug("failed to close registry key", + slog.Any("err", err), + ) + } + + return nil +} + func (c *Collector) collectNTP(ch chan<- prometheus.Metric) error { err := c.perfDataCollector.Collect(&c.perfDataObject) if err != nil { diff --git a/pkg/collector/collect.go b/pkg/collector/collect.go index c52fe268..a1a66604 100644 --- a/pkg/collector/collect.go +++ b/pkg/collector/collect.go @@ -208,20 +208,28 @@ func (c *Collection) collectCollector(ch chan<- prometheus.Metric, logger *slog. return pending } - if err != nil && !errors.Is(err, pdh.ErrNoData) && !errors.Is(err, types.ErrNoData) { - if errors.Is(err, pdh.ErrPerformanceCounterNotInitialized) || errors.Is(err, mi.MI_RESULT_INVALID_NAMESPACE) { - err = fmt.Errorf("%w. Check application logs from initialization pharse for more information", err) + slogAttrs := make([]slog.Attr, 0) + + if err != nil { + if !errors.Is(err, pdh.ErrNoData) && !errors.Is(err, types.ErrNoData) { + if errors.Is(err, pdh.ErrPerformanceCounterNotInitialized) || errors.Is(err, mi.MI_RESULT_INVALID_NAMESPACE) { + err = fmt.Errorf("%w. Check application logs from initialization pharse for more information", err) + } + + logger.LogAttrs(ctx, slog.LevelWarn, + fmt.Sprintf("collector %s failed after %s, resulting in %d metrics", name, duration, numMetrics), + slog.Any("err", err), + ) + + return failed } - logger.LogAttrs(ctx, slog.LevelWarn, - fmt.Sprintf("collector %s failed after %s, resulting in %d metrics", name, duration, numMetrics), - slog.Any("err", err), - ) - - return failed + slogAttrs = append(slogAttrs, slog.Any("err", err)) } - logger.LogAttrs(ctx, slog.LevelDebug, fmt.Sprintf("collector %s succeeded after %s, resulting in %d metrics", name, duration, numMetrics)) + logger.LogAttrs(ctx, slog.LevelDebug, fmt.Sprintf("collector %s succeeded after %s, resulting in %d metrics", name, duration, numMetrics), + slogAttrs..., + ) return success } diff --git a/tools/e2e-output.txt b/tools/e2e-output.txt index 2a916443..8582c870 100644 --- a/tools/e2e-output.txt +++ b/tools/e2e-output.txt @@ -125,7 +125,6 @@ windows_exporter_collector_success{collector="os"} 1 windows_exporter_collector_success{collector="pagefile"} 1 windows_exporter_collector_success{collector="performancecounter"} 1 windows_exporter_collector_success{collector="physical_disk"} 1 -windows_exporter_collector_success{collector="printer"} 1 windows_exporter_collector_success{collector="process"} 1 windows_exporter_collector_success{collector="scheduled_task"} 1 windows_exporter_collector_success{collector="service"} 1 @@ -148,7 +147,6 @@ windows_exporter_collector_timeout{collector="os"} 0 windows_exporter_collector_timeout{collector="pagefile"} 0 windows_exporter_collector_timeout{collector="performancecounter"} 0 windows_exporter_collector_timeout{collector="physical_disk"} 0 -windows_exporter_collector_timeout{collector="printer"} 0 windows_exporter_collector_timeout{collector="process"} 0 windows_exporter_collector_timeout{collector="scheduled_task"} 0 windows_exporter_collector_timeout{collector="service"} 0 @@ -355,10 +353,6 @@ windows_exporter_collector_timeout{collector="udp"} 0 # TYPE windows_physical_disk_write_seconds_total counter # HELP windows_physical_disk_writes_total The number of write operations on the disk (PhysicalDisk.DiskWritesPerSec) # TYPE windows_physical_disk_writes_total counter -# HELP windows_printer_job_count Number of jobs processed by the printer since the last reset -# TYPE windows_printer_job_count counter -# HELP windows_printer_status Printer status -# TYPE windows_printer_status gauge # HELP windows_scheduled_task_last_result The result that was returned the last time the registered task was run # TYPE windows_scheduled_task_last_result gauge windows_scheduled_task_last_result{task="/Microsoft/Windows/PLA/GAEvents"} 0 @@ -433,13 +427,15 @@ windows_service_state{name="Themes",state="stopped"} 0 # TYPE windows_tcp_segments_total counter # HELP windows_textfile_mtime_seconds Unixtime mtime of textfiles successfully read. # TYPE windows_textfile_mtime_seconds gauge +# HELP windows_time_clock_sync_source This value reflects the sync source of the system clock. +# TYPE windows_time_clock_sync_source gauge # HELP windows_time_clock_frequency_adjustment This value reflects the adjustment made to the local system clock frequency by W32Time in nominal clock units. This counter helps visualize the finer adjustments being made by W32time to synchronize the local clock. # TYPE windows_time_clock_frequency_adjustment gauge # HELP windows_time_clock_frequency_adjustment_ppb This value reflects the adjustment made to the local system clock frequency by W32Time in Parts Per Billion (PPB) units. 1 PPB adjustment imples the system clock was adjusted at a rate of 1 nanosecond per second. The smallest possible adjustment can vary and can be expected to be in the order of 100's of PPB. This counter helps visualize the finer actions being taken by W32time to synchronize the local clock. # TYPE windows_time_clock_frequency_adjustment_ppb gauge # HELP windows_time_computed_time_offset_seconds Absolute time offset between the system clock and the chosen time source, in seconds # TYPE windows_time_computed_time_offset_seconds gauge -# HELP windows_time_current_timestamp_seconds OperatingSystem.LocalDateTime +# HELP windows_time_current_timestamp_seconds Current time as reported by the operating system, in unix time. # TYPE windows_time_current_timestamp_seconds gauge # HELP windows_time_ntp_client_time_sources Active number of NTP Time sources being used by the client # TYPE windows_time_ntp_client_time_sources gauge @@ -449,7 +445,7 @@ windows_service_state{name="Themes",state="stopped"} 0 # TYPE windows_time_ntp_server_incoming_requests_total counter # HELP windows_time_ntp_server_outgoing_responses_total Total number of requests responded to by NTP server # TYPE windows_time_ntp_server_outgoing_responses_total counter -# HELP windows_time_timezone OperatingSystem.LocalDateTime +# HELP windows_time_timezone Current timezone as reported by the operating system. # TYPE windows_time_timezone gauge # HELP windows_udp_datagram_no_port_total Number of received UDP datagrams for which there was no application at the destination port # TYPE windows_udp_datagram_no_port_total counter diff --git a/tools/end-to-end-test.ps1 b/tools/end-to-end-test.ps1 index 4eb1888c..89746057 100644 --- a/tools/end-to-end-test.ps1 +++ b/tools/end-to-end-test.ps1 @@ -25,7 +25,7 @@ $skip_re = "^(go_|windows_exporter_build_info|windows_exporter_collector_duratio $exporter_proc = Start-Process ` -PassThru ` -FilePath ..\windows_exporter.exe ` - -ArgumentList "--log.level=debug","--web.disable-exporter-metrics","--collectors.enabled=[defaults],cpu_info,textfile,process,pagefile,performancecounter,scheduled_task,tcp,udp,time,system,service,logical_disk,printer,os,net,memory,logon,cache","--collector.process.include=explorer.exe","--collector.scheduled_task.include=.*GAEvents","--collector.service.include=Themes","--collector.textfile.directories=$($textfile_dir)",@" + -ArgumentList "--log.level=debug","--web.disable-exporter-metrics","--collectors.enabled=[defaults],cpu_info,textfile,process,pagefile,performancecounter,scheduled_task,tcp,udp,time,system,service,logical_disk,os,net,memory,logon,cache","--collector.process.include=explorer.exe","--collector.scheduled_task.include=.*GAEvents","--collector.service.include=Themes","--collector.textfile.directories=$($textfile_dir)",@" --collector.performancecounter.objects="[{\"name\":\"cpu\",\"object\":\"Processor Information\",\"instances\":[\"*\"],\"instance_label\":\"core\",\"counters\":[{\"name\":\"% Processor Time\",\"metric\":\"windows_performancecounter_processor_information_processor_time\",\"labels\":{\"state\":\"active\"}},{\"name\":\"% Idle Time\",\"metric\":\"windows_performancecounter_processor_information_processor_time\",\"labels\":{\"state\":\"idle\"}}]},{\"name\":\"memory\",\"object\":\"Memory\",\"counters\":[{\"name\":\"Cache Faults/sec\",\"type\":\"counter\"}]}]" "@ ` -WindowStyle Hidden `