From 7059878252cdcd2c10cf356833ab5ff53343d4f7 Mon Sep 17 00:00:00 2001 From: Anthony Mirabella Date: Tue, 23 Nov 2021 12:08:13 -0800 Subject: [PATCH] Feature gate CLI flag and visibility changes (#4368) * Expose feature gate registry type and its methods Signed-off-by: Anthony J Mirabella * Add CLI flags for feature gates Signed-off-by: Anthony J Mirabella * Add accessor for global feature gate registry Signed-off-by: Anthony J Mirabella * add canonical import comment Signed-off-by: Anthony J Mirabella * Revert "Add accessor for global feature gate registry" This reverts commit 23c957c166d300ecb0ae546e82cae4aeffb36728. * Revert "Expose feature gate registry type and its methods" This reverts commit df64ad23588b28b096ded999a39d3198d4cf49a6. Signed-off-by: Anthony J Mirabella * Add CHANGELOG entry, update docblock for Flags() Signed-off-by: Anthony J Mirabella --- CHANGELOG.md | 4 ++ service/command.go | 2 + service/featuregate/flags.go | 96 +++++++++++++++++++++++++++++++ service/featuregate/flags_test.go | 93 ++++++++++++++++++++++++++++++ service/flags.go | 2 + 5 files changed, 197 insertions(+) create mode 100644 service/featuregate/flags.go create mode 100644 service/featuregate/flags_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 04a9a960c31..aae866cd034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ - Add semconv 1.7.0 and 1.8.0 (#4452) +## 💡 Enhancements 💡 + +- Added `feature-gates` CLI flag for controlling feature gate state. (#4368) + ## v0.39.0 Beta ## 🛑 Breaking changes 🛑 diff --git a/service/command.go b/service/command.go index c449b93ed52..80b732617de 100644 --- a/service/command.go +++ b/service/command.go @@ -18,6 +18,7 @@ import ( "github.com/spf13/cobra" "go.opentelemetry.io/collector/config/configmapprovider" + "go.opentelemetry.io/collector/service/featuregate" ) // NewCommand constructs a new cobra.Command using the given Collector. @@ -28,6 +29,7 @@ func NewCommand(set CollectorSettings) *cobra.Command { Version: set.BuildInfo.Version, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { + featuregate.Apply(featuregate.GetFlags()) if set.ConfigMapProvider == nil { set.ConfigMapProvider = configmapprovider.NewDefault(getConfigFlag(), getSetFlag()) } diff --git a/service/featuregate/flags.go b/service/featuregate/flags.go new file mode 100644 index 00000000000..f614e9964d7 --- /dev/null +++ b/service/featuregate/flags.go @@ -0,0 +1,96 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package featuregate // import "go.opentelemetry.io/collector/service/featuregate" + +import ( + "flag" + "sort" + "strings" +) + +const gatesListCfg = "feature-gates" + +var gatesList = FlagValue{} + +// Flags adds CLI flags for managing feature gates to the provided FlagSet +// Feature gates can be configured with `--feature-gates=foo,-bar`. This would +// enable the `foo` feature gate and disable the `bar` feature gate. +func Flags(flags *flag.FlagSet) { + flags.Var( + gatesList, + gatesListCfg, + "Comma-delimited list of feature gate identifiers. Prefix with '-' to disable the feature. '+' or no prefix will enable the feature.") +} + +// GetFlags returns the FlagValue used with Flags() +func GetFlags() FlagValue { + return gatesList +} + +var _ flag.Value = (*FlagValue)(nil) + +// FlagValue implements the flag.Value interface and provides a mechanism for applying feature +// gate statuses to a Registry +type FlagValue map[string]bool + +// String returns a string representing the FlagValue +func (f FlagValue) String() string { + var t []string + for k, v := range f { + if v { + t = append(t, k) + } else { + t = append(t, "-"+k) + } + } + + // Sort the list of identifiers for consistent results + sort.Strings(t) + return strings.Join(t, ",") +} + +// Set applies the FlagValue encoded in the input string +func (f FlagValue) Set(s string) error { + return f.SetSlice(strings.Split(s, ",")) +} + +// SetSlice applies the feature gate statuses in the input slice to the FlagValue +func (f FlagValue) SetSlice(s []string) error { + for _, v := range s { + var id string + var val bool + switch v[0] { + case '-': + id = v[1:] + val = false + case '+': + id = v[1:] + val = true + default: + id = v + val = true + } + + if _, exists := f[id]; exists { + // If the status has already been set, ignore it + // This allows CLI flags, which are processed first + // to take precedence over config settings + continue + } + f[id] = val + } + + return nil +} diff --git a/service/featuregate/flags_test.go b/service/featuregate/flags_test.go new file mode 100644 index 00000000000..7bb151d216a --- /dev/null +++ b/service/featuregate/flags_test.go @@ -0,0 +1,93 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package featuregate + +import ( + "flag" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFlags(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ContinueOnError) + Flags(fs) + assert.Equal(t, gatesList, fs.Lookup(gatesListCfg).Value) +} + +func TestGetFlags(t *testing.T) { + assert.Equal(t, gatesList, GetFlags()) +} + +func TestFlagValue_basic(t *testing.T) { + for _, tc := range []struct { + name string + expected string + input FlagValue + }{ + { + name: "single item", + input: FlagValue{"foo": true}, + expected: "foo", + }, + { + name: "single disabled item", + input: FlagValue{"foo": false}, + expected: "-foo", + }, + { + name: "multiple items", + input: FlagValue{"foo": true, "bar": false}, + expected: "-bar,foo", + }, + } { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.input.String()) + v := FlagValue{} + assert.NoError(t, v.Set(tc.expected)) + assert.Equal(t, tc.input, v) + }) + } +} + +func TestFlagValue_SetSlice(t *testing.T) { + for _, tc := range []struct { + name string + input []string + expected FlagValue + }{ + { + name: "single item", + input: []string{"foo"}, + expected: FlagValue{"foo": true}, + }, + { + name: "multiple items", + input: []string{"foo", "-bar", "+baz"}, + expected: FlagValue{"foo": true, "bar": false, "baz": true}, + }, + { + name: "repeated items", + input: []string{"foo", "-bar", "-foo"}, + expected: FlagValue{"foo": true, "bar": false}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + v := FlagValue{} + assert.NoError(t, v.SetSlice(tc.input)) + assert.Equal(t, tc.expected, v) + }) + } +} diff --git a/service/flags.go b/service/flags.go index 2c133d3be3a..66221157be3 100644 --- a/service/flags.go +++ b/service/flags.go @@ -19,6 +19,7 @@ import ( "strings" "go.opentelemetry.io/collector/config/configtelemetry" + "go.opentelemetry.io/collector/service/featuregate" ) var ( @@ -52,6 +53,7 @@ func (s *stringArrayValue) String() string { func flags() *flag.FlagSet { flagSet := new(flag.FlagSet) configtelemetry.Flags(flagSet) + featuregate.Flags(flagSet) // At least until we can use a generic, i.e.: OpenCensus, metrics exporter // we default to Prometheus at port 8888, if not otherwise specified.