diff --git a/.chloggen/feature-command.yaml b/.chloggen/feature-command.yaml new file mode 100644 index 00000000000..5f7aab71ab4 --- /dev/null +++ b/.chloggen/feature-command.yaml @@ -0,0 +1,13 @@ +change_type: enhancement + +component: otelcol + +note: "Add feature command to display information about available features" + +issues: [11998] + +subtext: | + The feature command allows users to view detailed information about feature gates + including their status, stage, and description. + +change_logs: [user] diff --git a/otelcol/command.go b/otelcol/command.go index 6efd16df562..632603106f5 100644 --- a/otelcol/command.go +++ b/otelcol/command.go @@ -6,6 +6,9 @@ package otelcol // import "go.opentelemetry.io/collector/otelcol" import ( "errors" "flag" + "fmt" + "os" + "text/tabwriter" "github.com/spf13/cobra" @@ -36,6 +39,7 @@ func NewCommand(set CollectorSettings) *cobra.Command { return col.Run(cmd.Context()) }, } + rootCmd.AddCommand(newFeaturesCommand()) rootCmd.AddCommand(newComponentsCommand(set)) rootCmd.AddCommand(newValidateSubCommand(set, flagSet)) rootCmd.Flags().AddGoFlagSet(flagSet) @@ -63,3 +67,44 @@ func updateSettingsUsingFlags(set *CollectorSettings, flags *flag.FlagSet) error } return nil } + +func newFeaturesCommand() *cobra.Command { + return &cobra.Command{ + Use: "features [feature-id]", + Short: "Display feature gates information", + Long: "Display information about available feature gates and their status", + RunE: func(_ *cobra.Command, args []string) error { + if len(args) > 0 { + found := false + featuregate.GlobalRegistry().VisitAll(func(g *featuregate.Gate) { + if g.ID() == args[0] { + found = true + fmt.Printf("Feature: %s\n", g.ID()) + fmt.Printf("Enabled: %v\n", g.IsEnabled()) + fmt.Printf("Stage: %s\n", g.Stage()) + fmt.Printf("Description: %s\n", g.Description()) + fmt.Printf("From Version: %s\n", g.FromVersion()) + if g.ToVersion() != "" { + fmt.Printf("To Version: %s\n", g.ToVersion()) + } + } + }) + if !found { + return fmt.Errorf("feature %q not found", args[0]) + } + return nil + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintf(w, "ID\tEnabled\tStage\tDescription\n") + featuregate.GlobalRegistry().VisitAll(func(g *featuregate.Gate) { + fmt.Fprintf(w, "%s\t%v\t%s\t%s\n", + g.ID(), + g.IsEnabled(), + g.Stage(), + g.Description()) + }) + return w.Flush() + }, + } +} diff --git a/otelcol/command_test.go b/otelcol/command_test.go index 1a5680fe4ab..fa839826cb1 100644 --- a/otelcol/command_test.go +++ b/otelcol/command_test.go @@ -5,6 +5,8 @@ package otelcol import ( "context" + "io" + "os" "path/filepath" "testing" @@ -159,3 +161,59 @@ func Test_UseUnifiedEnvVarExpansionRules(t *testing.T) { }) } } + +func TestNewFeaturesCommand(t *testing.T) { + t.Run("list all features", func(t *testing.T) { + cmd := newFeaturesCommand() + require.NotNil(t, cmd) + + // Capture stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := cmd.RunE(cmd, []string{}) + require.NoError(t, err) + + w.Close() + out, _ := io.ReadAll(r) + os.Stdout = oldStdout + + output := string(out) + assert.Contains(t, output, "ID") + assert.Contains(t, output, "Enabled") + assert.Contains(t, output, "Stage") + assert.Contains(t, output, "Description") + }) + t.Run("specific feature details", func(t *testing.T) { + cmd := newFeaturesCommand() + + // Register a test feature gate in the global registry + featuregate.GlobalRegistry().MustRegister("test.feature", featuregate.StageBeta, + featuregate.WithRegisterDescription("Test feature description")) + + // Capture stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := cmd.RunE(cmd, []string{"test.feature"}) + require.NoError(t, err) + + w.Close() + out, _ := io.ReadAll(r) + os.Stdout = oldStdout + + output := string(out) + assert.Contains(t, output, "Feature: test.feature") + assert.Contains(t, output, "Description: Test feature description") + assert.Contains(t, output, "Stage: Beta") + }) + + t.Run("non-existent feature", func(t *testing.T) { + cmd := newFeaturesCommand() + err := cmd.RunE(cmd, []string{"non.existent.feature"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "feature \"non.existent.feature\" not found") + }) +}