Skip to content

Commit 6dd21a8

Browse files
authored
update: expose publish date of updates (#2050)
1 parent 92f213c commit 6dd21a8

File tree

2 files changed

+104
-23
lines changed

2 files changed

+104
-23
lines changed

docs/collector.update.md

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,23 @@ Define the interval of scraping Windows Update information
2626

2727
## Metrics
2828

29-
| Name | Description | Type | Labels |
30-
|--------------------------------|-------------------------------------------------------------|-------|-------------------------------|
31-
| `windows_update_pending_info` | Expose information for a single pending update item | gauge | `category`,`severity`,`title` |
32-
| `windows_update_scrape_query_duration_seconds` | Duration of the last scrape query to the Windows Update API | gauge | |
33-
| `windows_update_scrape_timestamp_seconds` | Timestamp of the last scrape | gauge | |
29+
| Name | Description | Type | Labels |
30+
|------------------------------------------------|------------------------------------------------------------------|-------|-------------------------------|
31+
| `windows_update_pending_info` | Expose information for a single pending update item | gauge | `category`,`severity`,`title` |
32+
| `windows_update_pending_published_timestamp` | Expose last published timestamp for a single pending update item | gauge | `title` |
33+
| `windows_update_scrape_query_duration_seconds` | Duration of the last scrape query to the Windows Update API | gauge | |
34+
| `windows_update_scrape_timestamp_seconds` | Timestamp of the last scrape | gauge | |
3435

3536
### Example metrics
3637
```
37-
# HELP windows_update_pending Pending Windows Updates
38-
# TYPE windows_update_pending gauge
39-
windows_update_pending{category="Drivers",severity="",title="Intel Corporation - Bluetooth - 23.60.5.10"} 1
38+
# HELP windows_update_pending_info Expose information for a single pending update item
39+
# TYPE windows_update_pending_info gauge
40+
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
41+
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
42+
# HELP windows_update_pending_published_timestamp Expose last published timestamp for a single pending update item
43+
# TYPE windows_update_pending_published_timestamp gauge
44+
windows_update_pending_published_timestamp{id="a32ca1d0-ddd4-486b-b708-d941db4f1051",revision="204"} 1.747872e+09
45+
windows_update_pending_published_timestamp{id="b50a64de-a0bb-465b-9842-9963b6eee21e",revision="200"} 1.7479584e+09
4046
# HELP windows_update_scrape_query_duration_seconds Duration of the last scrape query to the Windows Update API
4147
# TYPE windows_update_scrape_query_duration_seconds gauge
4248
windows_update_scrape_query_duration_seconds 2.8161838
@@ -46,7 +52,12 @@ windows_update_scrape_timestamp_seconds 1.727539734e+09
4652
```
4753

4854
## Useful queries
49-
_This collector does not yet have any useful queries added, we would appreciate your help adding them!_
55+
56+
Add extended information like cmdline or owner to other process metrics.
57+
58+
```
59+
windows_update_pending_published_timestamp * on(id, revision) group_left(severity, title) windows_update_pending_info
60+
```
5061

5162
## Alerting examples
5263
_This collector does not yet have alerting examples, we would appreciate your help adding them!_

internal/collector/update/update.go

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,14 @@ type Collector struct {
6060
mu sync.RWMutex
6161
ctxCancelFn context.CancelFunc
6262

63+
logger *slog.Logger
64+
6365
metricsBuf []prometheus.Metric
6466

65-
pendingUpdate *prometheus.Desc
66-
queryDurationSeconds *prometheus.Desc
67-
lastScrapeMetric *prometheus.Desc
67+
pendingUpdate *prometheus.Desc
68+
pendingUpdateLastPublished *prometheus.Desc
69+
queryDurationSeconds *prometheus.Desc
70+
lastScrapeMetric *prometheus.Desc
6871
}
6972

7073
func New(config *Config) *Collector {
@@ -146,9 +149,9 @@ func (c *Collector) Close() error {
146149
}
147150

148151
func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
149-
logger = logger.With(slog.String("collector", Name))
152+
c.logger = logger.With(slog.String("collector", Name))
150153

151-
logger.Info("update collector is in an experimental state! The configuration and metrics may change in future. Please report any issues.")
154+
c.logger.Info("update collector is in an experimental state! The configuration and metrics may change in future. Please report any issues.")
152155

153156
ctx, cancel := context.WithCancel(context.Background())
154157

@@ -164,7 +167,14 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
164167
c.pendingUpdate = prometheus.NewDesc(
165168
prometheus.BuildFQName(types.Namespace, Name, "pending_info"),
166169
"Expose information for a single pending update item",
167-
[]string{"category", "severity", "title"},
170+
[]string{"id", "revision", "category", "severity", "title"},
171+
nil,
172+
)
173+
174+
c.pendingUpdateLastPublished = prometheus.NewDesc(
175+
prometheus.BuildFQName(types.Namespace, Name, "pending_published_timestamp"),
176+
"Expose last published timestamp for a single pending update item",
177+
[]string{"id", "revision"},
168178
nil,
169179
)
170180

@@ -241,9 +251,16 @@ func (c *Collector) scheduleUpdateStatus(ctx context.Context, logger *slog.Logge
241251

242252
defer musQueryInterface.Release()
243253

254+
_, err = oleutil.PutProperty(musQueryInterface, "UserLocale", 1033)
255+
if err != nil {
256+
initErrCh <- fmt.Errorf("failed to set ClientApplicationID: %w", err)
257+
258+
return
259+
}
260+
244261
_, err = oleutil.PutProperty(musQueryInterface, "ClientApplicationID", "windows_exporter")
245262
if err != nil {
246-
initErrCh <- fmt.Errorf("put ClientApplicationID: %w", err)
263+
initErrCh <- fmt.Errorf("failed to set ClientApplicationID: %w", err)
247264

248265
return
249266
}
@@ -320,7 +337,7 @@ func (c *Collector) scheduleUpdateStatus(ctx context.Context, logger *slog.Logge
320337
}
321338

322339
func (c *Collector) fetchUpdates(logger *slog.Logger, usd *ole.IDispatch) ([]prometheus.Metric, error) {
323-
metricsBuf := make([]prometheus.Metric, 0, len(c.metricsBuf))
340+
metricsBuf := make([]prometheus.Metric, 0, len(c.metricsBuf)*2+1)
324341

325342
timeStart := time.Now()
326343

@@ -367,10 +384,22 @@ func (c *Collector) fetchUpdates(logger *slog.Logger, usd *ole.IDispatch) ([]pro
367384
c.pendingUpdate,
368385
prometheus.GaugeValue,
369386
1,
387+
update.identity,
388+
update.revision,
370389
update.category,
371390
update.severity,
372391
update.title,
373392
))
393+
394+
if update.lastPublished != (time.Time{}) {
395+
metricsBuf = append(metricsBuf, prometheus.MustNewConstMetric(
396+
c.pendingUpdateLastPublished,
397+
prometheus.GaugeValue,
398+
float64(update.lastPublished.Unix()),
399+
update.identity,
400+
update.revision,
401+
))
402+
}
374403
}
375404

376405
metricsBuf = append(metricsBuf, prometheus.MustNewConstMetric(
@@ -383,9 +412,12 @@ func (c *Collector) fetchUpdates(logger *slog.Logger, usd *ole.IDispatch) ([]pro
383412
}
384413

385414
type windowsUpdate struct {
386-
category string
387-
severity string
388-
title string
415+
identity string
416+
revision string
417+
category string
418+
severity string
419+
title string
420+
lastPublished time.Time
389421
}
390422

391423
// getUpdateStatus retrieves the update status of the given item.
@@ -423,10 +455,48 @@ func (c *Collector) getUpdateStatus(updd *ole.IDispatch, item int) (windowsUpdat
423455
return windowsUpdate{}, fmt.Errorf("get Title: %w", err)
424456
}
425457

458+
// Get the Identity object
459+
identityVariant, err := oleutil.GetProperty(updateItem, "Identity")
460+
if err != nil {
461+
return windowsUpdate{}, fmt.Errorf("get Identity: %w", err)
462+
}
463+
464+
identity := identityVariant.ToIDispatch()
465+
defer identity.Release()
466+
467+
// Read the UpdateID
468+
updateIDVariant, err := oleutil.GetProperty(identity, "UpdateID")
469+
if err != nil {
470+
return windowsUpdate{}, fmt.Errorf("get UpdateID: %w", err)
471+
}
472+
473+
revisionVariant, err := oleutil.GetProperty(identity, "RevisionNumber")
474+
if err != nil {
475+
return windowsUpdate{}, fmt.Errorf("get RevisionNumber: %w", err)
476+
}
477+
478+
lastPublished, err := oleutil.GetProperty(updateItem, "LastDeploymentChangeTime")
479+
if err != nil {
480+
return windowsUpdate{}, fmt.Errorf("get LastDeploymentChangeTime: %w", err)
481+
}
482+
483+
lastPublishedDate, err := ole.GetVariantDate(uint64(lastPublished.Val))
484+
if err != nil {
485+
c.logger.Debug("failed to convert LastDeploymentChangeTime",
486+
slog.String("title", title.ToString()),
487+
slog.Any("err", err),
488+
)
489+
490+
lastPublishedDate = time.Time{}
491+
}
492+
426493
return windowsUpdate{
427-
category: categoryName,
428-
severity: severity.ToString(),
429-
title: title.ToString(),
494+
identity: updateIDVariant.ToString(),
495+
revision: strconv.FormatInt(revisionVariant.Val, 10),
496+
category: categoryName,
497+
severity: severity.ToString(),
498+
title: title.ToString(),
499+
lastPublished: lastPublishedDate,
430500
}, nil
431501
}
432502

0 commit comments

Comments
 (0)