From 9c9b6804a3d84190ac61a8e811608cee97175999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Fri, 9 Oct 2026 11:14:57 +0200 Subject: [PATCH] collector: preserve fatal causes in joined build errors (#2599) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Otto Kröpke --- pkg/collector/collection.go | 37 +++++++++++++++++++++++++------- pkg/collector/collection_test.go | 4 ++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/pkg/collector/collection.go b/pkg/collector/collection.go index d424cfa7..cb4e2579 100644 --- a/pkg/collector/collection.go +++ b/pkg/collector/collection.go @@ -280,14 +280,7 @@ func (c *Collection) Build(ctx context.Context, logger *slog.Logger) error { // errors.ErrUnsupported marks a collector whose subsystem is not // available on this host, e.g. a Windows feature that is not installed. - if errors.Is(err, errors.ErrUnsupported) || - errors.Is(err, pdh.ErrNoData) || - errors.Is(err, winregistry.ErrNotExist) || - errors.Is(err, pdh.NewPdhError(pdh.CstatusNoObject)) || - errors.Is(err, pdh.NewPdhError(pdh.CstatusNoCounter)) || - errors.Is(err, mi.MI_RESULT_INVALID_OPERATION_TIMEOUT) || - errors.Is(err, mi.MI_RESULT_INVALID_NAMESPACE) || - errors.Is(err, mi.MI_RESULT_INVALID_CLASS) { + if expectedBuildError(err) { logger.LogAttrs(ctx, slog.LevelWarn, "couldn't initialize collector", slog.Any("err", err), ) @@ -421,3 +414,31 @@ func (c *Collection) WithCollectors(collectors []string) (*Collection, error) { func (c *Collection) GetStartTime() gotime.Time { return c.startTime } + +// expectedBuildError tolerates an error tree only if every independent cause is +// an expected initialization failure for an optional Windows subsystem. +func expectedBuildError(err error) bool { + if joined, ok := err.(interface{ Unwrap() []error }); ok { + causes := joined.Unwrap() + for _, cause := range causes { + if !expectedBuildError(cause) { + return false + } + } + + return len(causes) != 0 + } + + if wrapped, ok := err.(interface{ Unwrap() error }); ok { + return expectedBuildError(wrapped.Unwrap()) + } + + return errors.Is(err, errors.ErrUnsupported) || + errors.Is(err, pdh.ErrNoData) || + errors.Is(err, winregistry.ErrNotExist) || + errors.Is(err, pdh.NewPdhError(pdh.CstatusNoObject)) || + errors.Is(err, pdh.NewPdhError(pdh.CstatusNoCounter)) || + errors.Is(err, mi.MI_RESULT_INVALID_OPERATION_TIMEOUT) || + errors.Is(err, mi.MI_RESULT_INVALID_NAMESPACE) || + errors.Is(err, mi.MI_RESULT_INVALID_CLASS) +} diff --git a/pkg/collector/collection_test.go b/pkg/collector/collection_test.go index f5cd24ea..ec1d5e58 100644 --- a/pkg/collector/collection_test.go +++ b/pkg/collector/collection_test.go @@ -72,6 +72,10 @@ func TestCollectionBuildAndClose(t *testing.T) { {name: "optional counter missing", buildErr: pdh.ErrNoData}, {name: "unsupported on this host", buildErr: fmt.Errorf("feature missing: %w", errors.ErrUnsupported)}, {name: "close failure", closeErr: closeFailure}, + {name: "all expected joined errors", buildErr: errors.Join(errors.ErrUnsupported, pdh.ErrNoData)}, + {name: "joined fatal error", buildErr: errors.Join(errors.ErrUnsupported, buildFailure), wantBuildErr: buildFailure}, + {name: "nested fatal error", buildErr: errors.Join(pdh.ErrNoData, fmt.Errorf("nested: %w", errors.Join(errors.ErrUnsupported, buildFailure))), wantBuildErr: buildFailure}, + {name: "multiple wrapped fatal error", buildErr: fmt.Errorf("%w and %w", errors.ErrUnsupported, buildFailure), wantBuildErr: buildFailure}, } { t.Run(tc.name, func(t *testing.T) { t.Parallel()