diff --git a/docs/collector.update.md b/docs/collector.update.md index 44410afe..4512010f 100644 --- a/docs/collector.update.md +++ b/docs/collector.update.md @@ -26,17 +26,23 @@ Define the interval of scraping Windows Update information ## Metrics -| Name | Description | Type | Labels | -|--------------------------------|-------------------------------------------------------------|-------|-------------------------------| -| `windows_update_pending_info` | Expose information for a single pending update item | gauge | `category`,`severity`,`title` | -| `windows_update_scrape_query_duration_seconds` | Duration of the last scrape query to the Windows Update API | gauge | | -| `windows_update_scrape_timestamp_seconds` | Timestamp of the last scrape | gauge | | +| Name | Description | Type | Labels | +|------------------------------------------------|------------------------------------------------------------------|-------|-------------------------------| +| `windows_update_pending_info` | Expose information for a single pending update item | gauge | `category`,`severity`,`title` | +| `windows_update_pending_published_timestamp` | Expose last published timestamp for a single pending update item | gauge | `title` | +| `windows_update_scrape_query_duration_seconds` | Duration of the last scrape query to the Windows Update API | gauge | | +| `windows_update_scrape_timestamp_seconds` | Timestamp of the last scrape | gauge | | ### Example metrics ``` -# HELP windows_update_pending Pending Windows Updates -# TYPE windows_update_pending gauge -windows_update_pending{category="Drivers",severity="",title="Intel Corporation - Bluetooth - 23.60.5.10"} 1 +# HELP windows_update_pending_info Expose information for a single pending update item +# TYPE windows_update_pending_info gauge +windows_update_pending_info{category="Definition Updates",id="a32ca1d0-ddd4-486b-b708-d941db4f1051",revision="204",severity="",title="Update for Windows Security platform - KB5007651 (Version 10.0.27840.1000)"} 1 +windows_update_pending_info{category="Definition Updates",id="b50a64de-a0bb-465b-9842-9963b6eee21e",revision="200",severity="",title="Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.429.146.0) - Current Channel (Broad)"} 1 +# HELP windows_update_pending_published_timestamp Expose last published timestamp for a single pending update item +# TYPE windows_update_pending_published_timestamp gauge +windows_update_pending_published_timestamp{id="a32ca1d0-ddd4-486b-b708-d941db4f1051",revision="204"} 1.747872e+09 +windows_update_pending_published_timestamp{id="b50a64de-a0bb-465b-9842-9963b6eee21e",revision="200"} 1.7479584e+09 # HELP windows_update_scrape_query_duration_seconds Duration of the last scrape query to the Windows Update API # TYPE windows_update_scrape_query_duration_seconds gauge windows_update_scrape_query_duration_seconds 2.8161838 @@ -46,7 +52,12 @@ windows_update_scrape_timestamp_seconds 1.727539734e+09 ``` ## Useful queries -_This collector does not yet have any useful queries added, we would appreciate your help adding them!_ + +Add extended information like cmdline or owner to other process metrics. + +``` +windows_update_pending_published_timestamp * on(id, revision) group_left(severity, title) windows_update_pending_info +``` ## Alerting examples _This collector does not yet have alerting examples, we would appreciate your help adding them!_ diff --git a/internal/collector/update/update.go b/internal/collector/update/update.go index 49077a70..c068918b 100644 --- a/internal/collector/update/update.go +++ b/internal/collector/update/update.go @@ -60,11 +60,14 @@ type Collector struct { mu sync.RWMutex ctxCancelFn context.CancelFunc + logger *slog.Logger + metricsBuf []prometheus.Metric - pendingUpdate *prometheus.Desc - queryDurationSeconds *prometheus.Desc - lastScrapeMetric *prometheus.Desc + pendingUpdate *prometheus.Desc + pendingUpdateLastPublished *prometheus.Desc + queryDurationSeconds *prometheus.Desc + lastScrapeMetric *prometheus.Desc } func New(config *Config) *Collector { @@ -146,9 +149,9 @@ func (c *Collector) Close() error { } func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error { - logger = logger.With(slog.String("collector", Name)) + c.logger = logger.With(slog.String("collector", Name)) - logger.Info("update collector is in an experimental state! The configuration and metrics may change in future. Please report any issues.") + c.logger.Info("update collector is in an experimental state! The configuration and metrics may change in future. Please report any issues.") ctx, cancel := context.WithCancel(context.Background()) @@ -164,7 +167,14 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error { c.pendingUpdate = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "pending_info"), "Expose information for a single pending update item", - []string{"category", "severity", "title"}, + []string{"id", "revision", "category", "severity", "title"}, + nil, + ) + + c.pendingUpdateLastPublished = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "pending_published_timestamp"), + "Expose last published timestamp for a single pending update item", + []string{"id", "revision"}, nil, ) @@ -241,9 +251,16 @@ func (c *Collector) scheduleUpdateStatus(ctx context.Context, logger *slog.Logge defer musQueryInterface.Release() + _, err = oleutil.PutProperty(musQueryInterface, "UserLocale", 1033) + if err != nil { + initErrCh <- fmt.Errorf("failed to set ClientApplicationID: %w", err) + + return + } + _, err = oleutil.PutProperty(musQueryInterface, "ClientApplicationID", "windows_exporter") if err != nil { - initErrCh <- fmt.Errorf("put ClientApplicationID: %w", err) + initErrCh <- fmt.Errorf("failed to set ClientApplicationID: %w", err) return } @@ -320,7 +337,7 @@ func (c *Collector) scheduleUpdateStatus(ctx context.Context, logger *slog.Logge } func (c *Collector) fetchUpdates(logger *slog.Logger, usd *ole.IDispatch) ([]prometheus.Metric, error) { - metricsBuf := make([]prometheus.Metric, 0, len(c.metricsBuf)) + metricsBuf := make([]prometheus.Metric, 0, len(c.metricsBuf)*2+1) timeStart := time.Now() @@ -367,10 +384,22 @@ func (c *Collector) fetchUpdates(logger *slog.Logger, usd *ole.IDispatch) ([]pro c.pendingUpdate, prometheus.GaugeValue, 1, + update.identity, + update.revision, update.category, update.severity, update.title, )) + + if update.lastPublished != (time.Time{}) { + metricsBuf = append(metricsBuf, prometheus.MustNewConstMetric( + c.pendingUpdateLastPublished, + prometheus.GaugeValue, + float64(update.lastPublished.Unix()), + update.identity, + update.revision, + )) + } } metricsBuf = append(metricsBuf, prometheus.MustNewConstMetric( @@ -383,9 +412,12 @@ func (c *Collector) fetchUpdates(logger *slog.Logger, usd *ole.IDispatch) ([]pro } type windowsUpdate struct { - category string - severity string - title string + identity string + revision string + category string + severity string + title string + lastPublished time.Time } // getUpdateStatus retrieves the update status of the given item. @@ -423,10 +455,48 @@ func (c *Collector) getUpdateStatus(updd *ole.IDispatch, item int) (windowsUpdat return windowsUpdate{}, fmt.Errorf("get Title: %w", err) } + // Get the Identity object + identityVariant, err := oleutil.GetProperty(updateItem, "Identity") + if err != nil { + return windowsUpdate{}, fmt.Errorf("get Identity: %w", err) + } + + identity := identityVariant.ToIDispatch() + defer identity.Release() + + // Read the UpdateID + updateIDVariant, err := oleutil.GetProperty(identity, "UpdateID") + if err != nil { + return windowsUpdate{}, fmt.Errorf("get UpdateID: %w", err) + } + + revisionVariant, err := oleutil.GetProperty(identity, "RevisionNumber") + if err != nil { + return windowsUpdate{}, fmt.Errorf("get RevisionNumber: %w", err) + } + + lastPublished, err := oleutil.GetProperty(updateItem, "LastDeploymentChangeTime") + if err != nil { + return windowsUpdate{}, fmt.Errorf("get LastDeploymentChangeTime: %w", err) + } + + lastPublishedDate, err := ole.GetVariantDate(uint64(lastPublished.Val)) + if err != nil { + c.logger.Debug("failed to convert LastDeploymentChangeTime", + slog.String("title", title.ToString()), + slog.Any("err", err), + ) + + lastPublishedDate = time.Time{} + } + return windowsUpdate{ - category: categoryName, - severity: severity.ToString(), - title: title.ToString(), + identity: updateIDVariant.ToString(), + revision: strconv.FormatInt(revisionVariant.Val, 10), + category: categoryName, + severity: severity.ToString(), + title: title.ToString(), + lastPublished: lastPublishedDate, }, nil }