From 706232c80ddb84220c28e49e2f16e0c3ea7c5eac Mon Sep 17 00:00:00 2001 From: Brett Tofel Date: Fri, 18 Apr 2025 10:36:47 -0400 Subject: [PATCH 1/2] Log catalogd feature gate states --- cmd/catalogd/main.go | 3 +++ internal/catalogd/features/features.go | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/cmd/catalogd/main.go b/cmd/catalogd/main.go index 8bf083285..9499a7006 100644 --- a/cmd/catalogd/main.go +++ b/cmd/catalogd/main.go @@ -188,6 +188,9 @@ func validateConfig(cfg *config) error { } func run(ctx context.Context) error { + // log startup message and feature gate status + setupLog.Info("starting up catalogd", "version info", version.String()) + features.LogFeatureGateStates(setupLog, features.CatalogdFeatureGate) authFilePath := filepath.Join(os.TempDir(), fmt.Sprintf("%s-%s.json", authFilePrefix, apimachineryrand.String(8))) protocol := "http://" diff --git a/internal/catalogd/features/features.go b/internal/catalogd/features/features.go index 38c092a97..c5252655c 100644 --- a/internal/catalogd/features/features.go +++ b/internal/catalogd/features/features.go @@ -1,6 +1,9 @@ package features import ( + "sort" + + "github.com/go-logr/logr" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/component-base/featuregate" ) @@ -18,3 +21,21 @@ var CatalogdFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureG func init() { utilruntime.Must(CatalogdFeatureGate.Add(catalogdFeatureGates)) } + +// LogFeatureGateStates logs the state of all known feature gates for catalogd +func LogFeatureGateStates(log logr.Logger, fg featuregate.FeatureGate) { + // Sort the keys for consistent logging order + featureKeys := make([]featuregate.Feature, 0, len(catalogdFeatureGates)) + for k := range catalogdFeatureGates { + featureKeys = append(featureKeys, k) + } + sort.Slice(featureKeys, func(i, j int) bool { + return string(featureKeys[i]) < string(featureKeys[j]) + }) + + featurePairs := make([]interface{}, 0, len(featureKeys)) + for _, feature := range featureKeys { + featurePairs = append(featurePairs, feature, fg.Enabled(feature)) + } + log.Info("catalogd feature gate status", featurePairs...) +} From 275cf831071ab60856c814ca70cebe4e57aa7ec8 Mon Sep 17 00:00:00 2001 From: Brett Tofel Date: Mon, 21 Apr 2025 12:57:30 -0400 Subject: [PATCH 2/2] Unify feature-gate logs in shared util Signed-off-by: Brett Tofel --- internal/catalogd/features/features.go | 19 +---- .../operator-controller/features/features.go | 19 +---- internal/shared/util/featuregates/logging.go | 29 +++++++ .../shared/util/featuregates/logging_test.go | 78 +++++++++++++++++++ 4 files changed, 113 insertions(+), 32 deletions(-) create mode 100644 internal/shared/util/featuregates/logging.go create mode 100644 internal/shared/util/featuregates/logging_test.go diff --git a/internal/catalogd/features/features.go b/internal/catalogd/features/features.go index c5252655c..298cbc859 100644 --- a/internal/catalogd/features/features.go +++ b/internal/catalogd/features/features.go @@ -1,11 +1,11 @@ package features import ( - "sort" - "github.com/go-logr/logr" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/component-base/featuregate" + + fgutil "github.com/operator-framework/operator-controller/internal/shared/util/featuregates" ) const ( @@ -24,18 +24,5 @@ func init() { // LogFeatureGateStates logs the state of all known feature gates for catalogd func LogFeatureGateStates(log logr.Logger, fg featuregate.FeatureGate) { - // Sort the keys for consistent logging order - featureKeys := make([]featuregate.Feature, 0, len(catalogdFeatureGates)) - for k := range catalogdFeatureGates { - featureKeys = append(featureKeys, k) - } - sort.Slice(featureKeys, func(i, j int) bool { - return string(featureKeys[i]) < string(featureKeys[j]) - }) - - featurePairs := make([]interface{}, 0, len(featureKeys)) - for _, feature := range featureKeys { - featurePairs = append(featurePairs, feature, fg.Enabled(feature)) - } - log.Info("catalogd feature gate status", featurePairs...) + fgutil.LogFeatureGateStates(log, "catalogd feature gate status", fg, catalogdFeatureGates) } diff --git a/internal/operator-controller/features/features.go b/internal/operator-controller/features/features.go index 41645f62c..e8faa07f0 100644 --- a/internal/operator-controller/features/features.go +++ b/internal/operator-controller/features/features.go @@ -1,11 +1,11 @@ package features import ( - "sort" - "github.com/go-logr/logr" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/component-base/featuregate" + + fgutil "github.com/operator-framework/operator-controller/internal/shared/util/featuregates" ) const ( @@ -42,18 +42,5 @@ func init() { // LogFeatureGateStates logs the state of all known feature gates. func LogFeatureGateStates(log logr.Logger, fg featuregate.FeatureGate) { - // Sort the keys for consistent logging order - featureKeys := make([]featuregate.Feature, 0, len(operatorControllerFeatureGates)) - for k := range operatorControllerFeatureGates { - featureKeys = append(featureKeys, k) - } - sort.Slice(featureKeys, func(i, j int) bool { - return string(featureKeys[i]) < string(featureKeys[j]) // Sort by string representation - }) - - featurePairs := make([]interface{}, 0, len(featureKeys)) - for _, feature := range featureKeys { - featurePairs = append(featurePairs, feature, fg.Enabled(feature)) - } - log.Info("feature gate status", featurePairs...) + fgutil.LogFeatureGateStates(log, "feature gate status", fg, operatorControllerFeatureGates) } diff --git a/internal/shared/util/featuregates/logging.go b/internal/shared/util/featuregates/logging.go new file mode 100644 index 000000000..6649a350d --- /dev/null +++ b/internal/shared/util/featuregates/logging.go @@ -0,0 +1,29 @@ +package featuregates + +import ( + "sort" + + "github.com/go-logr/logr" + "k8s.io/component-base/featuregate" +) + +// LogFeatureGateStates logs a sorted list of features and their enabled state +// message is the log message under which to record the feature states +// fg is the feature gate instance, and featureDefs is the map of feature specs +func LogFeatureGateStates(log logr.Logger, message string, fg featuregate.FeatureGate, featureDefs map[featuregate.Feature]featuregate.FeatureSpec) { + // Collect and sort feature keys for deterministic ordering + keys := make([]featuregate.Feature, 0, len(featureDefs)) + for f := range featureDefs { + keys = append(keys, f) + } + sort.Slice(keys, func(i, j int) bool { + return string(keys[i]) < string(keys[j]) + }) + + // Build key/value pairs for logging + pairs := make([]interface{}, 0, len(keys)*2) + for _, f := range keys { + pairs = append(pairs, f, fg.Enabled(f)) + } + log.Info(message, pairs...) +} diff --git a/internal/shared/util/featuregates/logging_test.go b/internal/shared/util/featuregates/logging_test.go new file mode 100644 index 000000000..1d4b163e3 --- /dev/null +++ b/internal/shared/util/featuregates/logging_test.go @@ -0,0 +1,78 @@ +package featuregates_test + +import ( + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/require" + "k8s.io/component-base/featuregate" + + "github.com/operator-framework/operator-controller/internal/shared/util/featuregates" +) + +// fakeSink implements logr.LogSink, capturing Info calls for testing +type fakeSink struct { + level int + msg string + keysAndValues []interface{} +} + +// Init is part of logr.LogSink +func (f *fakeSink) Init(info logr.RuntimeInfo) {} + +// Enabled is part of logr.LogSink +func (f *fakeSink) Enabled(level int) bool { return true } + +// Info captures the log level, message, and key/value pairs +func (f *fakeSink) Info(level int, msg string, keysAndValues ...interface{}) { + f.level = level + f.msg = msg + f.keysAndValues = append([]interface{}{}, keysAndValues...) +} + +// Error is part of logr.LogSink; not used in this test +func (f *fakeSink) Error(err error, msg string, keysAndValues ...interface{}) {} + +// WithValues returns a sink with additional values; for testing, return self +func (f *fakeSink) WithValues(keysAndValues ...interface{}) logr.LogSink { return f } + +// WithName returns a sink with a new name; for testing, return self +func (f *fakeSink) WithName(name string) logr.LogSink { return f } + +// TestLogFeatureGateStates verifies that LogFeatureGateStates logs features +// sorted alphabetically with their enabled state +func TestLogFeatureGateStates(t *testing.T) { + // Define a set of feature specs with default states + defs := map[featuregate.Feature]featuregate.FeatureSpec{ + "AFeature": {Default: false}, + "BFeature": {Default: true}, + "CFeature": {Default: false}, + } + + // create a mutable gate and register our definitions + gate := featuregate.NewFeatureGate() + require.NoError(t, gate.Add(defs)) + + // override CFeature to true. + require.NoError(t, gate.SetFromMap(map[string]bool{ + "CFeature": true, + })) + + // prepare a fake sink and logger + sink := &fakeSink{} + logger := logr.New(sink) + + // log the feature states + featuregates.LogFeatureGateStates(logger, "feature states", gate, defs) + + // verify the message + require.Equal(t, "feature states", sink.msg) + + // Expect keys sorted: AFeature, BFeature, CFeature + want := []interface{}{ + featuregate.Feature("AFeature"), false, + featuregate.Feature("BFeature"), true, + featuregate.Feature("CFeature"), true, + } + require.Equal(t, want, sink.keysAndValues) +}