mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 10:09:05 +02:00
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:
co-authored by
Claude
parent
be5036dfa8
commit
e88c8a712c
@@ -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 {
|
||||
|
||||
@@ -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) }
|
||||
|
||||
Reference in New Issue
Block a user