diff --git a/config/config.go b/config/config.go index 1e4c400c86cba..8e59b97f5f4db 100644 --- a/config/config.go +++ b/config/config.go @@ -310,6 +310,11 @@ type AgentConfig struct { // metrics buffered in the last `flush_interval` in the event of a power // cut. BufferDiskSync *bool `toml:"buffer_disk_sync"` + + // AllowPluginMissingFields, when true, logs a warning for configuration + // keys that are not used by the target table (agent, tags, or plugin) and + // continues loading instead of returning an error. + AllowPluginMissingFields bool `toml:"allow_plugin_missing_fields"` } // InputNames returns a list of strings of the configured inputs. @@ -680,11 +685,13 @@ func (c *Config) LoadConfigData(data []byte, path string) error { } } - if len(c.UnusedFields) > 0 { - return fmt.Errorf( - "line %d: configuration specified the fields %q, but they were not used; "+ - "this is either a typo or this config option does not exist in this version", - tbl.Line, keys(c.UnusedFields)) + err = c.unusedFieldsErrorOrWarn( + "line %d: configuration specified the fields %q, but they were not used; "+ + "this is either a typo or this config option does not exist in this version", + tbl.Line, + ) + if err != nil { + return err } // Initialize the file-sorting slices @@ -718,11 +725,13 @@ func (c *Config) LoadConfigData(data []byte, path string) error { return fmt.Errorf("unsupported config format: %s", pluginName) } - if len(c.UnusedFields) > 0 { - return fmt.Errorf( - "plugin %s.%s: line %d: configuration specified the fields %q, but they were not used; "+ - "this is either a typo or this config option does not exist in this version", - name, pluginName, subTable.Line, keys(c.UnusedFields)) + err = c.unusedFieldsErrorOrWarn( + "plugin %s.%s: line %d: configuration specified the fields %q, but they were not used; "+ + "this is either a typo or this config option does not exist in this version", + name, pluginName, subTable.Line, + ) + if err != nil { + return err } } case "inputs", "plugins": @@ -743,11 +752,13 @@ func (c *Config) LoadConfigData(data []byte, path string) error { return fmt.Errorf("unsupported config format: %s", pluginName) } - if len(c.UnusedFields) > 0 { - return fmt.Errorf( - "plugin %s.%s: line %d: configuration specified the fields %q, but they were not used; "+ - "this is either a typo or this config option does not exist in this version", - name, pluginName, subTable.Line, keys(c.UnusedFields)) + err = c.unusedFieldsErrorOrWarn( + "plugin %s.%s: line %d: configuration specified the fields %q, but they were not used; "+ + "this is either a typo or this config option does not exist in this version", + name, pluginName, subTable.Line, + ) + if err != nil { + return err } } case "processors": @@ -763,15 +774,13 @@ func (c *Config) LoadConfigData(data []byte, path string) error { return fmt.Errorf("unsupported config format: %s", pluginName) } - if len(c.UnusedFields) > 0 { - return fmt.Errorf( - "plugin %s.%s: line %d: configuration specified the fields %q, but they were not used; "+ - "this is either a typo or this config option does not exist in this version", - name, - pluginName, - subTable.Line, - keys(c.UnusedFields), - ) + err = c.unusedFieldsErrorOrWarn( + "plugin %s.%s: line %d: configuration specified the fields %q, but they were not used; "+ + "this is either a typo or this config option does not exist in this version", + name, pluginName, subTable.Line, + ) + if err != nil { + return err } } case "aggregators": @@ -787,11 +796,13 @@ func (c *Config) LoadConfigData(data []byte, path string) error { return fmt.Errorf("unsupported config format: %s", pluginName) } - if len(c.UnusedFields) > 0 { - return fmt.Errorf( - "plugin %s.%s: line %d: configuration specified the fields %q, but they were not used; "+ - "this is either a typo or this config option does not exist in this version", - name, pluginName, subTable.Line, keys(c.UnusedFields)) + err = c.unusedFieldsErrorOrWarn( + "plugin %s.%s: line %d: configuration specified the fields %q, but they were not used; "+ + "this is either a typo or this config option does not exist in this version", + name, pluginName, subTable.Line, + ) + if err != nil { + return err } } case "secretstores": @@ -806,10 +817,13 @@ func (c *Config) LoadConfigData(data []byte, path string) error { default: return fmt.Errorf("unsupported config format: %s", pluginName) } - if len(c.UnusedFields) > 0 { - msg := "plugin %s.%s: line %d: configuration specified the fields %q, but they were not used; " + - "this is either a typo or this config option does not exist in this version" - return fmt.Errorf(msg, name, pluginName, subTable.Line, keys(c.UnusedFields)) + err = c.unusedFieldsErrorOrWarn( + "plugin %s.%s: line %d: configuration specified the fields %q, but they were not used; "+ + "this is either a typo or this config option does not exist in this version", + name, pluginName, subTable.Line, + ) + if err != nil { + return err } } @@ -2026,6 +2040,26 @@ func (c *Config) matchesLabelSelection(tbl *ast.Table) (bool, error) { return pluginLabelSelector.matches(labels), nil } +// unusedFieldsErrorOrWarn fails load with a formatted error when UnusedFields +// is non-empty and AllowPluginMissingFields is false. When AllowPluginMissingFields +// is true, it logs a warning and returns nil. +func (c *Config) unusedFieldsErrorOrWarn(format string, args ...any) error { + c.unusedFieldsMutex.Lock() + if len(c.UnusedFields) == 0 { + c.unusedFieldsMutex.Unlock() + return nil + } + fieldKeys := keys(c.UnusedFields) + c.unusedFieldsMutex.Unlock() + allow := c.Agent != nil && c.Agent.AllowPluginMissingFields + fmtArgs := append(append([]any(nil), args...), fieldKeys) + if allow { + log.Printf("W! "+format, fmtArgs...) + return nil + } + return fmt.Errorf(format, fmtArgs...) +} + func keys(m map[string]bool) []string { result := make([]string, 0, len(m)) for k := range m { diff --git a/config/config_test.go b/config/config_test.go index 35b428bd1128d..d1cfb87af4f10 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -455,6 +455,77 @@ func TestConfig_FieldNotDefined(t *testing.T) { } } +func TestConfig_FieldNotDefined_AllowPluginMissingFields(t *testing.T) { + tests := []struct { + name string + filename string + minInputs int + minProcessors int + }{ + { + name: "in input plugin without parser", + filename: "./testdata/invalid_field.toml", + minInputs: 1, + }, + { + name: "in input plugin with parser", + filename: "./testdata/invalid_field_with_parser.toml", + minInputs: 1, + }, + { + name: "in input plugin with parser func", + filename: "./testdata/invalid_field_with_parserfunc.toml", + minInputs: 1, + }, + { + name: "in parser of input plugin", + filename: "./testdata/invalid_field_in_parser_table.toml", + minInputs: 1, + }, + { + name: "in parser of input plugin with parser-func", + filename: "./testdata/invalid_field_in_parserfunc_table.toml", + minInputs: 1, + }, + { + name: "in processor plugin without parser", + filename: "./testdata/invalid_field_processor.toml", + minProcessors: 1, + }, + { + name: "in processor plugin with parser", + filename: "./testdata/invalid_field_processor_with_parser.toml", + minProcessors: 1, + }, + { + name: "in processor plugin with parser func", + filename: "./testdata/invalid_field_processor_with_parserfunc.toml", + minProcessors: 1, + }, + { + name: "in parser of processor plugin", + filename: "./testdata/invalid_field_processor_in_parser_table.toml", + minProcessors: 1, + }, + { + name: "in parser of processor plugin with parser-func", + filename: "./testdata/invalid_field_processor_in_parserfunc_table.toml", + minInputs: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := config.NewConfig() + c.Agent.AllowPluginMissingFields = true + err := c.LoadConfig(tt.filename) + require.NoError(t, err) + require.GreaterOrEqual(t, len(c.Inputs), tt.minInputs) + require.GreaterOrEqual(t, len(c.Processors), tt.minProcessors) + }) + } +} + func TestConfig_WrongFieldType(t *testing.T) { c := config.NewConfig() err := c.LoadConfig("./testdata/wrong_field_type.toml")