update: include optional updates such as the monthly preview update (#2688)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jan-Otto Kröpke
2026-10-11 20:47:53 +02:00
committed by GitHub
co-authored by Claude
parent be5036dfa8
commit e88c8a712c
7 changed files with 410 additions and 68 deletions
+13 -7
View File
@@ -9,24 +9,25 @@ function(on)
b.tab(
'Updates',
b.rows([
if on('update') then b.summary([210, 211, 212, 214, 215]),
if on('update') then b.summary([210, 211, 212, 213, 214, 215]),
if on('update') then b.row('Pending updates', b.flow([[216, 24, 10]])),
if on('update') then b.row('History', b.flow([[217, 12, 8], [218, 12, 8]])),
]),
[
b.stat(210, 'Pending', 'Updates that are available but not installed.' + requirement, pending('')),
b.stat(210, 'Pending', 'Updates that are available but not installed, including optional updates.' + requirement, pending('')),
b.stat(211, 'Critical', 'Pending updates with the Microsoft security severity Critical.' + requirement, pending('severity="Critical"'), steps=[{ color: 'green', value: null }, { color: 'red', value: 1 }]),
b.stat(212, 'Important', 'Pending updates with the Microsoft security severity Important.' + requirement, pending('severity="Important"'), steps=[{ color: 'green', value: null }, { color: 'orange', value: 1 }]),
b.stat(214, 'Oldest pending', 'Time since the oldest pending update was published. Orange after 30 days, red after 60 days.' + requirement, 'time() - min(' + metric('pending_published_timestamp') + ')', 'dtdurations', steps=[{ color: 'green', value: null }, { color: 'orange', value: days(30) }, { color: 'red', value: days(60) }])
b.stat(213, 'Optional', 'Pending optional updates, such as the monthly non-security preview update. Windows does not install them automatically.' + requirement, pending('deployment_action="optional_installation"')),
b.stat(214, 'Oldest pending', 'Time since the oldest pending regular update was published. Optional updates are left out. Orange after 30 days, red after 60 days.' + requirement, 'time() - min(' + metric('pending_published_timestamp') + ' and on (id, revision) ' + metric('pending_info', 'deployment_action="installation"') + ')', 'dtdurations', steps=[{ color: 'green', value: null }, { color: 'orange', value: days(30) }, { color: 'red', value: days(60) }])
+ b.panel.withDefaults({ noValue: 'none' })
+ b.panel.withOptions({ reduceOptions: { calcs: ['last'] } }),
b.stat(215, 'Last check', 'Time since the collector last queried Windows Update. Orange after 1 day, red after 7 days.' + requirement, 'time() - ' + metric('scrape_timestamp_seconds'), 'dtdurations', steps=[{ color: 'green', value: null }, { color: 'orange', value: days(1) }, { color: 'red', value: days(7) }])
+ b.panel.withOptions({ reduceOptions: { calcs: ['last'] } }),
b.panel.new(216, 'Pending updates', 'table')
+ b.panel.withDescription('Pending updates with their category, security severity and publish date. Category names follow the Windows display language.' + requirement)
+ b.panel.withDescription('Pending updates with their category, security severity, type and publish date. Optional updates, such as the monthly non-security preview update, are not installed automatically. Category names follow the Windows display language.' + requirement)
+ b.panel.withQueries([
b.query.new('max by (title, category, severity) (' + '(' + metric('pending_published_timestamp') + ' * on (id, revision) group_right ' + metric('pending_info') + ') or on (id, revision) (0 * ' + metric('pending_info') + ')) * 1000', 'A')
b.query.new('max by (title, category, severity, deployment_action) (' + '(' + metric('pending_published_timestamp') + ' * on (id, revision) group_right ' + metric('pending_info') + ') or on (id, revision) (0 * ' + metric('pending_info') + ')) * 1000', 'A')
+ b.query.withInstant()
+ b.query.withFormat('table'),
])
@@ -37,8 +38,8 @@ function(on)
options: {
excludeByName: { Time: true },
includeByName: {},
indexByName: { title: 0, category: 1, severity: 2, Value: 3 },
renameByName: { title: 'Update', category: 'Category', severity: 'Severity', Value: 'Published' },
indexByName: { title: 0, category: 1, severity: 2, deployment_action: 3, Value: 4 },
renameByName: { title: 'Update', category: 'Category', severity: 'Severity', deployment_action: 'Type', Value: 'Published' },
},
},
}])
@@ -51,6 +52,11 @@ function(on)
{ id: 'mappings', value: [{ type: 'value', options: { Critical: { color: 'red', index: 0 }, Important: { color: 'orange', index: 1 } } }] },
{ id: 'custom.cellOptions', value: { type: 'color-text' } },
],
}, {
matcher: { id: 'byName', options: 'Type' },
properties: [
{ id: 'mappings', value: [{ type: 'value', options: { installation: { index: 0, text: 'Regular' }, optional_installation: { index: 1, text: 'Optional' } } }] },
],
}]),
b.graph(217, 'Pending updates by category', 'Number of pending updates per update category.' + requirement, 'short', [
+139 -13
View File
@@ -12728,7 +12728,7 @@
"transformations": []
}
},
"description": "Updates that are available but not installed. Requires the update collector, which is not enabled by default. The collector queries Windows Update in the background, every 6 hours by default.",
"description": "Updates that are available but not installed, including optional updates. Requires the update collector, which is not enabled by default. The collector queries Windows Update in the background, every 6 hours by default.",
"id": 210,
"links": [],
"title": "Pending",
@@ -12953,6 +12953,91 @@
}
}
},
"panel-213": {
"kind": "Panel",
"spec": {
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"hidden": false,
"query": {
"datasource": {
"name": "${datasource}"
},
"group": "prometheus",
"kind": "DataQuery",
"spec": {
"editorMode": "code",
"expr": "count(windows_update_pending_info{job=~\"$job\", instance=\"$instance\", deployment_action=\"optional_installation\"}) or (0 * windows_update_scrape_timestamp_seconds{job=~\"$job\", instance=\"$instance\"})",
"instant": false,
"legendFormat": "__auto",
"range": true
},
"version": "v0"
},
"refId": "A"
}
}
],
"queryOptions": {
"maxDataPoints": 100
},
"transformations": []
}
},
"description": "Pending optional updates, such as the monthly non-security preview update. Windows does not install them automatically. Requires the update collector, which is not enabled by default. The collector queries Windows Update in the background, every 6 hours by default.",
"id": 213,
"links": [],
"title": "Optional",
"vizConfig": {
"group": "stat",
"kind": "VizConfig",
"spec": {
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "text",
"mode": "fixed"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "text",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
}
},
"version": ""
}
}
},
"panel-214": {
"kind": "Panel",
"spec": {
@@ -12972,7 +13057,7 @@
"kind": "DataQuery",
"spec": {
"editorMode": "code",
"expr": "time() - min(windows_update_pending_published_timestamp{job=~\"$job\", instance=\"$instance\"})",
"expr": "time() - min(windows_update_pending_published_timestamp{job=~\"$job\", instance=\"$instance\"} and on (id, revision) windows_update_pending_info{job=~\"$job\", instance=\"$instance\", deployment_action=\"installation\"})",
"instant": false,
"legendFormat": "__auto",
"range": true
@@ -12989,7 +13074,7 @@
"transformations": []
}
},
"description": "Time since the oldest pending update was published. Orange after 30 days, red after 60 days. Requires the update collector, which is not enabled by default. The collector queries Windows Update in the background, every 6 hours by default.",
"description": "Time since the oldest pending regular update was published. Optional updates are left out. Orange after 30 days, red after 60 days. Requires the update collector, which is not enabled by default. The collector queries Windows Update in the background, every 6 hours by default.",
"id": 214,
"links": [],
"title": "Oldest pending",
@@ -13157,7 +13242,7 @@
"kind": "DataQuery",
"spec": {
"editorMode": "code",
"expr": "max by (title, category, severity) ((windows_update_pending_published_timestamp{job=~\"$job\", instance=\"$instance\"} * on (id, revision) group_right windows_update_pending_info{job=~\"$job\", instance=\"$instance\"}) or on (id, revision) (0 * windows_update_pending_info{job=~\"$job\", instance=\"$instance\"})) * 1000",
"expr": "max by (title, category, severity, deployment_action) ((windows_update_pending_published_timestamp{job=~\"$job\", instance=\"$instance\"} * on (id, revision) group_right windows_update_pending_info{job=~\"$job\", instance=\"$instance\"}) or on (id, revision) (0 * windows_update_pending_info{job=~\"$job\", instance=\"$instance\"})) * 1000",
"format": "table",
"instant": true,
"legendFormat": "__auto",
@@ -13181,14 +13266,16 @@
},
"includeByName": {},
"indexByName": {
"Value": 3,
"Value": 4,
"category": 1,
"deployment_action": 3,
"severity": 2,
"title": 0
},
"renameByName": {
"Value": "Published",
"category": "Category",
"deployment_action": "Type",
"severity": "Severity",
"title": "Update"
}
@@ -13198,7 +13285,7 @@
]
}
},
"description": "Pending updates with their category, security severity and publish date. Category names follow the Windows display language. Requires the update collector, which is not enabled by default. The collector queries Windows Update in the background, every 6 hours by default.",
"description": "Pending updates with their category, security severity, type and publish date. Optional updates, such as the monthly non-security preview update, are not installed automatically. Category names follow the Windows display language. Requires the update collector, which is not enabled by default. The collector queries Windows Update in the background, every 6 hours by default.",
"id": 216,
"links": [],
"title": "Pending updates",
@@ -13281,6 +13368,32 @@
}
}
]
},
{
"matcher": {
"id": "byName",
"options": "Type"
},
"properties": [
{
"id": "mappings",
"value": [
{
"options": {
"installation": {
"index": 0,
"text": "Regular"
},
"optional_installation": {
"index": 1,
"text": "Optional"
}
},
"type": "value"
}
]
}
]
}
]
},
@@ -29025,7 +29138,7 @@
"name": "panel-210"
},
"height": 4,
"width": 5,
"width": 4,
"x": 0,
"y": 0
}
@@ -29038,8 +29151,8 @@
"name": "panel-211"
},
"height": 4,
"width": 5,
"x": 5,
"width": 4,
"x": 4,
"y": 0
}
},
@@ -29051,8 +29164,21 @@
"name": "panel-212"
},
"height": 4,
"width": 5,
"x": 10,
"width": 4,
"x": 8,
"y": 0
}
},
{
"kind": "GridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-213"
},
"height": 4,
"width": 4,
"x": 12,
"y": 0
}
},
@@ -29064,8 +29190,8 @@
"name": "panel-214"
},
"height": 4,
"width": 5,
"x": 15,
"width": 4,
"x": 16,
"y": 0
}
},
+139 -13
View File
@@ -95,7 +95,7 @@
"transformations": []
}
},
"description": "Updates that are available but not installed. Requires the update collector, which is not enabled by default. The collector queries Windows Update in the background, every 6 hours by default.",
"description": "Updates that are available but not installed, including optional updates. Requires the update collector, which is not enabled by default. The collector queries Windows Update in the background, every 6 hours by default.",
"id": 210,
"links": [],
"title": "Pending",
@@ -320,6 +320,91 @@
}
}
},
"panel-213": {
"kind": "Panel",
"spec": {
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"hidden": false,
"query": {
"datasource": {
"name": "${datasource}"
},
"group": "prometheus",
"kind": "DataQuery",
"spec": {
"editorMode": "code",
"expr": "count(windows_update_pending_info{job=~\"$job\", instance=\"$instance\", deployment_action=\"optional_installation\"}) or (0 * windows_update_scrape_timestamp_seconds{job=~\"$job\", instance=\"$instance\"})",
"instant": false,
"legendFormat": "__auto",
"range": true
},
"version": "v0"
},
"refId": "A"
}
}
],
"queryOptions": {
"maxDataPoints": 100
},
"transformations": []
}
},
"description": "Pending optional updates, such as the monthly non-security preview update. Windows does not install them automatically. Requires the update collector, which is not enabled by default. The collector queries Windows Update in the background, every 6 hours by default.",
"id": 213,
"links": [],
"title": "Optional",
"vizConfig": {
"group": "stat",
"kind": "VizConfig",
"spec": {
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "text",
"mode": "fixed"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "text",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
}
},
"version": ""
}
}
},
"panel-214": {
"kind": "Panel",
"spec": {
@@ -339,7 +424,7 @@
"kind": "DataQuery",
"spec": {
"editorMode": "code",
"expr": "time() - min(windows_update_pending_published_timestamp{job=~\"$job\", instance=\"$instance\"})",
"expr": "time() - min(windows_update_pending_published_timestamp{job=~\"$job\", instance=\"$instance\"} and on (id, revision) windows_update_pending_info{job=~\"$job\", instance=\"$instance\", deployment_action=\"installation\"})",
"instant": false,
"legendFormat": "__auto",
"range": true
@@ -356,7 +441,7 @@
"transformations": []
}
},
"description": "Time since the oldest pending update was published. Orange after 30 days, red after 60 days. Requires the update collector, which is not enabled by default. The collector queries Windows Update in the background, every 6 hours by default.",
"description": "Time since the oldest pending regular update was published. Optional updates are left out. Orange after 30 days, red after 60 days. Requires the update collector, which is not enabled by default. The collector queries Windows Update in the background, every 6 hours by default.",
"id": 214,
"links": [],
"title": "Oldest pending",
@@ -524,7 +609,7 @@
"kind": "DataQuery",
"spec": {
"editorMode": "code",
"expr": "max by (title, category, severity) ((windows_update_pending_published_timestamp{job=~\"$job\", instance=\"$instance\"} * on (id, revision) group_right windows_update_pending_info{job=~\"$job\", instance=\"$instance\"}) or on (id, revision) (0 * windows_update_pending_info{job=~\"$job\", instance=\"$instance\"})) * 1000",
"expr": "max by (title, category, severity, deployment_action) ((windows_update_pending_published_timestamp{job=~\"$job\", instance=\"$instance\"} * on (id, revision) group_right windows_update_pending_info{job=~\"$job\", instance=\"$instance\"}) or on (id, revision) (0 * windows_update_pending_info{job=~\"$job\", instance=\"$instance\"})) * 1000",
"format": "table",
"instant": true,
"legendFormat": "__auto",
@@ -548,14 +633,16 @@
},
"includeByName": {},
"indexByName": {
"Value": 3,
"Value": 4,
"category": 1,
"deployment_action": 3,
"severity": 2,
"title": 0
},
"renameByName": {
"Value": "Published",
"category": "Category",
"deployment_action": "Type",
"severity": "Severity",
"title": "Update"
}
@@ -565,7 +652,7 @@
]
}
},
"description": "Pending updates with their category, security severity and publish date. Category names follow the Windows display language. Requires the update collector, which is not enabled by default. The collector queries Windows Update in the background, every 6 hours by default.",
"description": "Pending updates with their category, security severity, type and publish date. Optional updates, such as the monthly non-security preview update, are not installed automatically. Category names follow the Windows display language. Requires the update collector, which is not enabled by default. The collector queries Windows Update in the background, every 6 hours by default.",
"id": 216,
"links": [],
"title": "Pending updates",
@@ -648,6 +735,32 @@
}
}
]
},
{
"matcher": {
"id": "byName",
"options": "Type"
},
"properties": [
{
"id": "mappings",
"value": [
{
"options": {
"installation": {
"index": 0,
"text": "Regular"
},
"optional_installation": {
"index": 1,
"text": "Optional"
}
},
"type": "value"
}
]
}
]
}
]
},
@@ -922,7 +1035,7 @@
"name": "panel-210"
},
"height": 4,
"width": 5,
"width": 4,
"x": 0,
"y": 0
}
@@ -935,8 +1048,8 @@
"name": "panel-211"
},
"height": 4,
"width": 5,
"x": 5,
"width": 4,
"x": 4,
"y": 0
}
},
@@ -948,8 +1061,21 @@
"name": "panel-212"
},
"height": 4,
"width": 5,
"x": 10,
"width": 4,
"x": 8,
"y": 0
}
},
{
"kind": "GridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-213"
},
"height": 4,
"width": 4,
"x": 12,
"y": 0
}
},
@@ -961,8 +1087,8 @@
"name": "panel-214"
},
"height": 4,
"width": 5,
"x": 15,
"width": 4,
"x": 16,
"y": 0
}
},
+18 -8
View File
@@ -27,23 +27,27 @@ Default: `6h0m0s`
## Metrics
| Name | Description | Type | Labels |
|------------------------------------------------|------------------------------------------------------------------|-------|-----------------------------------------------|
| `windows_update_pending_info` | Expose information for a single pending update item | gauge | `id`,`revision`,`category`,`severity`,`title` |
| `windows_update_pending_published_timestamp` | Expose last published timestamp for a single pending update item | gauge | `id`,`revision` |
| `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 | |
Pending updates include optional updates, such as the monthly non-security preview update. The `deployment_action` label is `installation` for regular updates and `optional_installation` for optional ones. Filter with `deployment_action="installation"` to ignore optional updates.
| Name | Description | Type | Labels |
|------------------------------------------------|------------------------------------------------------------------|-------|-------------------------------------------------------------------|
| `windows_update_pending_info` | Expose information for a single pending update item | gauge | `id`,`revision`,`category`,`severity`,`title`,`deployment_action` |
| `windows_update_pending_published_timestamp` | Expose last published timestamp for a single pending update item | gauge | `id`,`revision` |
| `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_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
windows_update_pending_info{category="Definition Updates",deployment_action="installation",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",deployment_action="installation",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
windows_update_pending_info{category="Updates",deployment_action="optional_installation",id="4c5a2079-dd6c-4b78-a20b-07b2d5ea2217",revision="1",severity="",title="2026-09 Preview Update (KB5124010) (26300.9550)"} 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
windows_update_pending_published_timestamp{id="4c5a2079-dd6c-4b78-a20b-07b2d5ea2217",revision="1"} 1.7900352e+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
@@ -60,5 +64,11 @@ 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
```
Count pending updates without optional updates, such as the monthly non-security preview update.
```
count(windows_update_pending_info{deployment_action="installation"})
```
## Alerting examples
_This collector does not yet have alerting examples, we would appreciate your help adding them!_
+51 -14
View File
@@ -50,6 +50,13 @@ var ConfigDefaults = Config{
ScrapeInterval: 6 * time.Hour,
}
// searchCriteria finds pending updates of both deployment actions. Criteria
// without a DeploymentAction clause imply DeploymentAction='Installation' and
// miss optional updates, such as the monthly non-security preview update.
// OR is only allowed at the top level of a WUA criteria string.
const searchCriteria = "IsInstalled=0 and IsHidden=0 and DeploymentAction='Installation'" +
" or IsInstalled=0 and IsHidden=0 and DeploymentAction='OptionalInstallation'"
var (
ErrNoUpdates = errors.New("pending gather update metrics")
ErrUpdateServiceDisabled = errors.New("windows updates service is disabled")
@@ -118,7 +125,7 @@ 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{"id", "revision", "category", "severity", "title"},
[]string{"id", "revision", "category", "severity", "title", "deployment_action"},
nil,
)
@@ -289,7 +296,7 @@ func (c *Collector) fetchUpdates(logger *slog.Logger, searcher *wuapi.UpdateSear
timeStart := time.Now()
result, err := searcher.Search("IsInstalled=0 and IsHidden=0")
result, err := searcher.Search(searchCriteria)
if err != nil {
return nil, fmt.Errorf("search for updates: %w", err)
}
@@ -310,6 +317,10 @@ func (c *Collector) fetchUpdates(logger *slog.Logger, searcher *wuapi.UpdateSear
}
defer updates.Release()
// Both criteria clauses are evaluated separately, so an update could be
// returned twice. Its publication metric must be emitted only once.
seen := make(map[string]struct{})
for item, err := range updates.All() {
if err != nil {
if _, ok := errors.AsType[*ole.CollectionItemError](err); !ok {
@@ -330,6 +341,13 @@ func (c *Collector) fetchUpdates(logger *slog.Logger, searcher *wuapi.UpdateSear
continue
}
key := update.identity + "/" + update.revision
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
metricsBuf = append(metricsBuf, prometheus.MustNewConstMetric(
c.pendingUpdate,
prometheus.GaugeValue,
@@ -339,6 +357,7 @@ func (c *Collector) fetchUpdates(logger *slog.Logger, searcher *wuapi.UpdateSear
update.category,
update.severity,
update.title,
update.deploymentAction,
))
if update.lastPublished != (time.Time{}) {
@@ -362,12 +381,13 @@ func (c *Collector) fetchUpdates(logger *slog.Logger, searcher *wuapi.UpdateSear
}
type windowsUpdate struct {
identity string
revision string
category string
severity string
title string
lastPublished time.Time
identity string
revision string
category string
severity string
title string
deploymentAction string
lastPublished time.Time
}
// getUpdateStatus retrieves the update status of the given item.
@@ -389,6 +409,11 @@ func (c *Collector) getUpdateStatus(item *wuapi.Update) (windowsUpdate, error) {
return windowsUpdate{}, fmt.Errorf("get Title: %w", err)
}
deploymentAction, err := item.DeploymentAction()
if err != nil {
return windowsUpdate{}, fmt.Errorf("get DeploymentAction: %w", err)
}
identity, err := item.Identity()
if err != nil {
return windowsUpdate{}, fmt.Errorf("get Identity: %w", err)
@@ -421,15 +446,27 @@ func (c *Collector) getUpdateStatus(item *wuapi.Update) (windowsUpdate, error) {
}
return windowsUpdate{
identity: updateID,
revision: strconv.FormatInt(int64(revision), 10),
category: categoryName,
severity: severity,
title: title,
lastPublished: lastPublishedDate,
identity: updateID,
revision: strconv.FormatInt(int64(revision), 10),
category: categoryName,
severity: severity,
title: title,
deploymentAction: deploymentActionLabel(deploymentAction),
lastPublished: lastPublishedDate,
}, nil
}
func deploymentActionLabel(action wuapi.DeploymentAction) string {
switch action {
case wuapi.DeploymentActionInstallation:
return "installation"
case wuapi.DeploymentActionOptionalInstallation:
return "optional_installation"
default:
return strconv.FormatInt(int64(action), 10)
}
}
func getUpdateCategory(update *wuapi.Update) (string, error) {
categories, err := update.Categories()
if err != nil {
@@ -38,7 +38,7 @@ func TestFetchUpdatesPartialCollection(t *testing.T) {
name string
countError bool
}{
{name: "skip unreadable item"},
{name: "skip unreadable and duplicate items"},
{name: "count failure", countError: true},
} {
t.Run(tc.name, func(t *testing.T) {
@@ -47,13 +47,13 @@ func TestFetchUpdatesPartialCollection(t *testing.T) {
logger := slog.New(slog.NewTextHandler(&logs, nil))
c := &Collector{
logger: logger,
pendingUpdate: prometheus.NewDesc("test_pending", "", []string{"id", "revision", "category", "severity", "title"}, nil),
pendingUpdate: prometheus.NewDesc("test_pending", "", []string{"id", "revision", "category", "severity", "title", "deployment_action"}, nil),
pendingUpdateLastPublished: prometheus.NewDesc("test_published", "", []string{"id", "revision"}, nil),
queryDurationSeconds: prometheus.NewDesc("test_duration", "", nil, nil),
lastScrapeMetric: prometheus.NewDesc("test_timestamp", "", nil, nil),
}
searcher, attempted := fakeUpdateSearch(tc.countError)
searcher, attempted, criteria := fakeUpdateSearch(tc.countError)
metrics, err := c.fetchUpdates(logger, searcher)
if tc.countError {
@@ -65,10 +65,12 @@ func TestFetchUpdatesPartialCollection(t *testing.T) {
}
require.NoError(t, err)
require.Equal(t, []int32{0, 1, 2}, *attempted)
require.Len(t, metrics, 6, "two readable updates retain info and publication metrics plus scrape metrics")
require.Equal(t, searchCriteria, *criteria)
require.Equal(t, []int32{0, 1, 2, 3}, *attempted)
require.Len(t, metrics, 6, "two distinct readable updates retain info and publication metrics plus scrape metrics")
ids := []string{}
actions := []string{}
for _, metric := range metrics {
if metric.Desc() != c.pendingUpdate {
@@ -79,13 +81,17 @@ func TestFetchUpdatesPartialCollection(t *testing.T) {
require.NoError(t, metric.Write(&sample))
for _, label := range sample.GetLabel() {
if label.GetName() == "id" {
switch label.GetName() {
case "id":
ids = append(ids, label.GetValue())
case "deployment_action":
actions = append(actions, label.GetValue())
}
}
}
require.Equal(t, []string{"update-0", "update-2"}, ids)
require.Equal(t, []string{"installation", "optional_installation"}, actions)
require.Contains(t, logs.String(), "failed to fetch Windows Update history item")
require.Contains(t, logs.String(), "get collection item 1")
})
@@ -93,11 +99,15 @@ func TestFetchUpdatesPartialCollection(t *testing.T) {
}
// fakeUpdateSearch implements only the native slots exercised by a scrape.
// Item 1 cannot be read; both surrounding items remain available.
func fakeUpdateSearch(countError bool) (*wuapi.UpdateSearcher, *[]int32) {
// Item 1 cannot be read; both surrounding items remain available. Item 0 is a
// regular update and item 2 an optional one. Item 3 repeats the identity of
// item 0. The search criteria are recorded.
func fakeUpdateSearch(countError bool) (*wuapi.UpdateSearcher, *[]int32, *string) {
release := windows.NewCallback(func(uintptr) uintptr { return 0 })
attempted := []int32{}
var criteria string
var selected int32
var categoryMethods [10]uintptr
@@ -119,11 +129,11 @@ func fakeUpdateSearch(countError bool) (*wuapi.UpdateSearcher, *[]int32) {
return 0
})
identityMethods[8] = windows.NewCallback(func(_ uintptr, out **uint16) uintptr {
return fakeBSTR(fmt.Sprintf("update-%d", selected), out)
return fakeBSTR(fmt.Sprintf("update-%d", selected%3), out)
})
identity := &wuapi.UpdateIdentity{VTable: &identityMethods[0]}
var updateMethods [35]uintptr
var updateMethods [49]uintptr
updateMethods[2] = release
updateMethods[7] = windows.NewCallback(func(_ uintptr, out **uint16) uintptr {
@@ -147,6 +157,14 @@ func fakeUpdateSearch(countError bool) (*wuapi.UpdateSearcher, *[]int32) {
updateMethods[34] = windows.NewCallback(func(_ uintptr, out **uint16) uintptr {
return fakeBSTR("Critical", out)
})
updateMethods[48] = windows.NewCallback(func(_ uintptr, out *wuapi.DeploymentAction) uintptr {
*out = wuapi.DeploymentActionInstallation
if selected == 2 {
*out = wuapi.DeploymentActionOptionalInstallation
}
return 0
})
update := &wuapi.Update{VTable: &updateMethods[0]}
var collectionMethods [11]uintptr
@@ -169,7 +187,7 @@ func fakeUpdateSearch(countError bool) (*wuapi.UpdateSearcher, *[]int32) {
return 0x80070005
}
*out = 3
*out = 4
return 0
})
@@ -188,13 +206,14 @@ func fakeUpdateSearch(countError bool) (*wuapi.UpdateSearcher, *[]int32) {
var searcherMethods [20]uintptr
searcherMethods[2] = release
searcherMethods[19] = windows.NewCallback(func(_ uintptr, _ *uint16, out **wuapi.SearchResult) uintptr {
searcherMethods[19] = windows.NewCallback(func(_ uintptr, input *uint16, out **wuapi.SearchResult) uintptr {
criteria = windows.UTF16PtrToString(input)
*out = result
return 0
})
return &wuapi.UpdateSearcher{VTable: &searcherMethods[0]}, &attempted
return &wuapi.UpdateSearcher{VTable: &searcherMethods[0]}, &attempted, &criteria
}
func fakeBSTR(value string, out **uint16) uintptr {
+18
View File
@@ -42,6 +42,7 @@ const (
updateIdentity = 19
updateDeploymentTime = 30
updateSeverity = 34
updateDeploymentAction = 48
identityRevision = 7
identityUpdateID = 8
categoryCollectionItem = 7
@@ -113,6 +114,23 @@ func (u *Update) Identity() (*UpdateIdentity, error) {
func (u *Update) LastDeploymentChangeTime() (ole.DATE, error) {
return u.Get[ole.DATE](updateDeploymentTime)
}
// DeploymentAction is the WUA DeploymentAction enumeration.
// https://learn.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-deploymentaction
type DeploymentAction int32
const (
DeploymentActionNone DeploymentAction = 0
DeploymentActionInstallation DeploymentAction = 1
DeploymentActionUninstallation DeploymentAction = 2
DeploymentActionDetection DeploymentAction = 3
DeploymentActionOptionalInstallation DeploymentAction = 4
)
func (u *Update) DeploymentAction() (DeploymentAction, error) {
return u.Get[DeploymentAction](updateDeploymentAction)
}
func (i *UpdateIdentity) UpdateID() (string, error) { return i.String(identityUpdateID) }
func (i *UpdateIdentity) RevisionNumber() (int32, error) { return i.Get[int32](identityRevision) }
func (c *Category) Name() (string, error) { return c.String(categoryName) }