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( b.tab(
'Updates', 'Updates',
b.rows([ 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('Pending updates', b.flow([[216, 24, 10]])),
if on('update') then b.row('History', b.flow([[217, 12, 8], [218, 12, 8]])), 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(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(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.withDefaults({ noValue: 'none' })
+ b.panel.withOptions({ reduceOptions: { calcs: ['last'] } }), + 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.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.withOptions({ reduceOptions: { calcs: ['last'] } }),
b.panel.new(216, 'Pending updates', 'table') 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.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.withInstant()
+ b.query.withFormat('table'), + b.query.withFormat('table'),
]) ])
@@ -37,8 +38,8 @@ function(on)
options: { options: {
excludeByName: { Time: true }, excludeByName: { Time: true },
includeByName: {}, includeByName: {},
indexByName: { title: 0, category: 1, severity: 2, Value: 3 }, indexByName: { title: 0, category: 1, severity: 2, deployment_action: 3, Value: 4 },
renameByName: { title: 'Update', category: 'Category', severity: 'Severity', Value: 'Published' }, 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: 'mappings', value: [{ type: 'value', options: { Critical: { color: 'red', index: 0 }, Important: { color: 'orange', index: 1 } } }] },
{ id: 'custom.cellOptions', value: { type: 'color-text' } }, { 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', [ 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": [] "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, "id": 210,
"links": [], "links": [],
"title": "Pending", "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": { "panel-214": {
"kind": "Panel", "kind": "Panel",
"spec": { "spec": {
@@ -12972,7 +13057,7 @@
"kind": "DataQuery", "kind": "DataQuery",
"spec": { "spec": {
"editorMode": "code", "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, "instant": false,
"legendFormat": "__auto", "legendFormat": "__auto",
"range": true "range": true
@@ -12989,7 +13074,7 @@
"transformations": [] "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, "id": 214,
"links": [], "links": [],
"title": "Oldest pending", "title": "Oldest pending",
@@ -13157,7 +13242,7 @@
"kind": "DataQuery", "kind": "DataQuery",
"spec": { "spec": {
"editorMode": "code", "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", "format": "table",
"instant": true, "instant": true,
"legendFormat": "__auto", "legendFormat": "__auto",
@@ -13181,14 +13266,16 @@
}, },
"includeByName": {}, "includeByName": {},
"indexByName": { "indexByName": {
"Value": 3, "Value": 4,
"category": 1, "category": 1,
"deployment_action": 3,
"severity": 2, "severity": 2,
"title": 0 "title": 0
}, },
"renameByName": { "renameByName": {
"Value": "Published", "Value": "Published",
"category": "Category", "category": "Category",
"deployment_action": "Type",
"severity": "Severity", "severity": "Severity",
"title": "Update" "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, "id": 216,
"links": [], "links": [],
"title": "Pending updates", "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" "name": "panel-210"
}, },
"height": 4, "height": 4,
"width": 5, "width": 4,
"x": 0, "x": 0,
"y": 0 "y": 0
} }
@@ -29038,8 +29151,8 @@
"name": "panel-211" "name": "panel-211"
}, },
"height": 4, "height": 4,
"width": 5, "width": 4,
"x": 5, "x": 4,
"y": 0 "y": 0
} }
}, },
@@ -29051,8 +29164,21 @@
"name": "panel-212" "name": "panel-212"
}, },
"height": 4, "height": 4,
"width": 5, "width": 4,
"x": 10, "x": 8,
"y": 0
}
},
{
"kind": "GridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-213"
},
"height": 4,
"width": 4,
"x": 12,
"y": 0 "y": 0
} }
}, },
@@ -29064,8 +29190,8 @@
"name": "panel-214" "name": "panel-214"
}, },
"height": 4, "height": 4,
"width": 5, "width": 4,
"x": 15, "x": 16,
"y": 0 "y": 0
} }
}, },
+139 -13
View File
@@ -95,7 +95,7 @@
"transformations": [] "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, "id": 210,
"links": [], "links": [],
"title": "Pending", "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": { "panel-214": {
"kind": "Panel", "kind": "Panel",
"spec": { "spec": {
@@ -339,7 +424,7 @@
"kind": "DataQuery", "kind": "DataQuery",
"spec": { "spec": {
"editorMode": "code", "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, "instant": false,
"legendFormat": "__auto", "legendFormat": "__auto",
"range": true "range": true
@@ -356,7 +441,7 @@
"transformations": [] "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, "id": 214,
"links": [], "links": [],
"title": "Oldest pending", "title": "Oldest pending",
@@ -524,7 +609,7 @@
"kind": "DataQuery", "kind": "DataQuery",
"spec": { "spec": {
"editorMode": "code", "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", "format": "table",
"instant": true, "instant": true,
"legendFormat": "__auto", "legendFormat": "__auto",
@@ -548,14 +633,16 @@
}, },
"includeByName": {}, "includeByName": {},
"indexByName": { "indexByName": {
"Value": 3, "Value": 4,
"category": 1, "category": 1,
"deployment_action": 3,
"severity": 2, "severity": 2,
"title": 0 "title": 0
}, },
"renameByName": { "renameByName": {
"Value": "Published", "Value": "Published",
"category": "Category", "category": "Category",
"deployment_action": "Type",
"severity": "Severity", "severity": "Severity",
"title": "Update" "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, "id": 216,
"links": [], "links": [],
"title": "Pending updates", "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" "name": "panel-210"
}, },
"height": 4, "height": 4,
"width": 5, "width": 4,
"x": 0, "x": 0,
"y": 0 "y": 0
} }
@@ -935,8 +1048,8 @@
"name": "panel-211" "name": "panel-211"
}, },
"height": 4, "height": 4,
"width": 5, "width": 4,
"x": 5, "x": 4,
"y": 0 "y": 0
} }
}, },
@@ -948,8 +1061,21 @@
"name": "panel-212" "name": "panel-212"
}, },
"height": 4, "height": 4,
"width": 5, "width": 4,
"x": 10, "x": 8,
"y": 0
}
},
{
"kind": "GridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-213"
},
"height": 4,
"width": 4,
"x": 12,
"y": 0 "y": 0
} }
}, },
@@ -961,8 +1087,8 @@
"name": "panel-214" "name": "panel-214"
}, },
"height": 4, "height": 4,
"width": 5, "width": 4,
"x": 15, "x": 16,
"y": 0 "y": 0
} }
}, },
+18 -8
View File
@@ -27,23 +27,27 @@ Default: `6h0m0s`
## Metrics ## Metrics
| Name | Description | Type | Labels | 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.
|------------------------------------------------|------------------------------------------------------------------|-------|-----------------------------------------------|
| `windows_update_pending_info` | Expose information for a single pending update item | gauge | `id`,`revision`,`category`,`severity`,`title` | | Name | Description | Type | Labels |
| `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_pending_info` | Expose information for a single pending update item | gauge | `id`,`revision`,`category`,`severity`,`title`,`deployment_action` |
| `windows_update_scrape_timestamp_seconds` | Timestamp of the last scrape | gauge | | | `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 ### Example metrics
``` ```
# HELP windows_update_pending_info Expose information for a single pending update item # HELP windows_update_pending_info Expose information for a single pending update item
# TYPE windows_update_pending_info gauge # 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",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",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="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 # HELP windows_update_pending_published_timestamp Expose last published timestamp for a single pending update item
# TYPE windows_update_pending_published_timestamp gauge # 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="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="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 # 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 # TYPE windows_update_scrape_query_duration_seconds gauge
windows_update_scrape_query_duration_seconds 2.8161838 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 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 ## Alerting examples
_This collector does not yet have alerting examples, we would appreciate your help adding them!_ _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, 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 ( var (
ErrNoUpdates = errors.New("pending gather update metrics") ErrNoUpdates = errors.New("pending gather update metrics")
ErrUpdateServiceDisabled = errors.New("windows updates service is disabled") 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( c.pendingUpdate = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "pending_info"), prometheus.BuildFQName(types.Namespace, Name, "pending_info"),
"Expose information for a single pending update item", "Expose information for a single pending update item",
[]string{"id", "revision", "category", "severity", "title"}, []string{"id", "revision", "category", "severity", "title", "deployment_action"},
nil, nil,
) )
@@ -289,7 +296,7 @@ func (c *Collector) fetchUpdates(logger *slog.Logger, searcher *wuapi.UpdateSear
timeStart := time.Now() timeStart := time.Now()
result, err := searcher.Search("IsInstalled=0 and IsHidden=0") result, err := searcher.Search(searchCriteria)
if err != nil { if err != nil {
return nil, fmt.Errorf("search for updates: %w", err) 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() 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() { for item, err := range updates.All() {
if err != nil { if err != nil {
if _, ok := errors.AsType[*ole.CollectionItemError](err); !ok { if _, ok := errors.AsType[*ole.CollectionItemError](err); !ok {
@@ -330,6 +341,13 @@ func (c *Collector) fetchUpdates(logger *slog.Logger, searcher *wuapi.UpdateSear
continue continue
} }
key := update.identity + "/" + update.revision
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
metricsBuf = append(metricsBuf, prometheus.MustNewConstMetric( metricsBuf = append(metricsBuf, prometheus.MustNewConstMetric(
c.pendingUpdate, c.pendingUpdate,
prometheus.GaugeValue, prometheus.GaugeValue,
@@ -339,6 +357,7 @@ func (c *Collector) fetchUpdates(logger *slog.Logger, searcher *wuapi.UpdateSear
update.category, update.category,
update.severity, update.severity,
update.title, update.title,
update.deploymentAction,
)) ))
if update.lastPublished != (time.Time{}) { if update.lastPublished != (time.Time{}) {
@@ -362,12 +381,13 @@ func (c *Collector) fetchUpdates(logger *slog.Logger, searcher *wuapi.UpdateSear
} }
type windowsUpdate struct { type windowsUpdate struct {
identity string identity string
revision string revision string
category string category string
severity string severity string
title string title string
lastPublished time.Time deploymentAction string
lastPublished time.Time
} }
// getUpdateStatus retrieves the update status of the given item. // 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) 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() identity, err := item.Identity()
if err != nil { if err != nil {
return windowsUpdate{}, fmt.Errorf("get Identity: %w", err) return windowsUpdate{}, fmt.Errorf("get Identity: %w", err)
@@ -421,15 +446,27 @@ func (c *Collector) getUpdateStatus(item *wuapi.Update) (windowsUpdate, error) {
} }
return windowsUpdate{ return windowsUpdate{
identity: updateID, identity: updateID,
revision: strconv.FormatInt(int64(revision), 10), revision: strconv.FormatInt(int64(revision), 10),
category: categoryName, category: categoryName,
severity: severity, severity: severity,
title: title, title: title,
lastPublished: lastPublishedDate, deploymentAction: deploymentActionLabel(deploymentAction),
lastPublished: lastPublishedDate,
}, nil }, 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) { func getUpdateCategory(update *wuapi.Update) (string, error) {
categories, err := update.Categories() categories, err := update.Categories()
if err != nil { if err != nil {
@@ -38,7 +38,7 @@ func TestFetchUpdatesPartialCollection(t *testing.T) {
name string name string
countError bool countError bool
}{ }{
{name: "skip unreadable item"}, {name: "skip unreadable and duplicate items"},
{name: "count failure", countError: true}, {name: "count failure", countError: true},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
@@ -47,13 +47,13 @@ func TestFetchUpdatesPartialCollection(t *testing.T) {
logger := slog.New(slog.NewTextHandler(&logs, nil)) logger := slog.New(slog.NewTextHandler(&logs, nil))
c := &Collector{ c := &Collector{
logger: logger, 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), pendingUpdateLastPublished: prometheus.NewDesc("test_published", "", []string{"id", "revision"}, nil),
queryDurationSeconds: prometheus.NewDesc("test_duration", "", nil, nil), queryDurationSeconds: prometheus.NewDesc("test_duration", "", nil, nil),
lastScrapeMetric: prometheus.NewDesc("test_timestamp", "", 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) metrics, err := c.fetchUpdates(logger, searcher)
if tc.countError { if tc.countError {
@@ -65,10 +65,12 @@ func TestFetchUpdatesPartialCollection(t *testing.T) {
} }
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []int32{0, 1, 2}, *attempted) require.Equal(t, searchCriteria, *criteria)
require.Len(t, metrics, 6, "two readable updates retain info and publication metrics plus scrape metrics") 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{} ids := []string{}
actions := []string{}
for _, metric := range metrics { for _, metric := range metrics {
if metric.Desc() != c.pendingUpdate { if metric.Desc() != c.pendingUpdate {
@@ -79,13 +81,17 @@ func TestFetchUpdatesPartialCollection(t *testing.T) {
require.NoError(t, metric.Write(&sample)) require.NoError(t, metric.Write(&sample))
for _, label := range sample.GetLabel() { for _, label := range sample.GetLabel() {
if label.GetName() == "id" { switch label.GetName() {
case "id":
ids = append(ids, label.GetValue()) 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{"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(), "failed to fetch Windows Update history item")
require.Contains(t, logs.String(), "get collection item 1") 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. // fakeUpdateSearch implements only the native slots exercised by a scrape.
// Item 1 cannot be read; both surrounding items remain available. // Item 1 cannot be read; both surrounding items remain available. Item 0 is a
func fakeUpdateSearch(countError bool) (*wuapi.UpdateSearcher, *[]int32) { // 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 }) release := windows.NewCallback(func(uintptr) uintptr { return 0 })
attempted := []int32{} attempted := []int32{}
var criteria string
var selected int32 var selected int32
var categoryMethods [10]uintptr var categoryMethods [10]uintptr
@@ -119,11 +129,11 @@ func fakeUpdateSearch(countError bool) (*wuapi.UpdateSearcher, *[]int32) {
return 0 return 0
}) })
identityMethods[8] = windows.NewCallback(func(_ uintptr, out **uint16) uintptr { 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]} identity := &wuapi.UpdateIdentity{VTable: &identityMethods[0]}
var updateMethods [35]uintptr var updateMethods [49]uintptr
updateMethods[2] = release updateMethods[2] = release
updateMethods[7] = windows.NewCallback(func(_ uintptr, out **uint16) uintptr { 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 { updateMethods[34] = windows.NewCallback(func(_ uintptr, out **uint16) uintptr {
return fakeBSTR("Critical", out) 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]} update := &wuapi.Update{VTable: &updateMethods[0]}
var collectionMethods [11]uintptr var collectionMethods [11]uintptr
@@ -169,7 +187,7 @@ func fakeUpdateSearch(countError bool) (*wuapi.UpdateSearcher, *[]int32) {
return 0x80070005 return 0x80070005
} }
*out = 3 *out = 4
return 0 return 0
}) })
@@ -188,13 +206,14 @@ func fakeUpdateSearch(countError bool) (*wuapi.UpdateSearcher, *[]int32) {
var searcherMethods [20]uintptr var searcherMethods [20]uintptr
searcherMethods[2] = release 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 *out = result
return 0 return 0
}) })
return &wuapi.UpdateSearcher{VTable: &searcherMethods[0]}, &attempted return &wuapi.UpdateSearcher{VTable: &searcherMethods[0]}, &attempted, &criteria
} }
func fakeBSTR(value string, out **uint16) uintptr { func fakeBSTR(value string, out **uint16) uintptr {
+18
View File
@@ -42,6 +42,7 @@ const (
updateIdentity = 19 updateIdentity = 19
updateDeploymentTime = 30 updateDeploymentTime = 30
updateSeverity = 34 updateSeverity = 34
updateDeploymentAction = 48
identityRevision = 7 identityRevision = 7
identityUpdateID = 8 identityUpdateID = 8
categoryCollectionItem = 7 categoryCollectionItem = 7
@@ -113,6 +114,23 @@ func (u *Update) Identity() (*UpdateIdentity, error) {
func (u *Update) LastDeploymentChangeTime() (ole.DATE, error) { func (u *Update) LastDeploymentChangeTime() (ole.DATE, error) {
return u.Get[ole.DATE](updateDeploymentTime) 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) UpdateID() (string, error) { return i.String(identityUpdateID) }
func (i *UpdateIdentity) RevisionNumber() (int32, error) { return i.Get[int32](identityRevision) } func (i *UpdateIdentity) RevisionNumber() (int32, error) { return i.Get[int32](identityRevision) }
func (c *Category) Name() (string, error) { return c.String(categoryName) } func (c *Category) Name() (string, error) { return c.String(categoryName) }