From 754b7ff4c14b6dc11722c79e397894844993571e Mon Sep 17 00:00:00 2001 From: Sven Rebhan <36194019+srebhan@users.noreply.github.com> Date: Mon, 26 Jul 2021 19:36:09 +0200 Subject: [PATCH] Example input plugin (#9409) --- .../{EXAMPLE_README.md => example/README.md} | 0 plugins/inputs/example/example.go | 136 ++++++ plugins/inputs/example/example_test.go | 439 ++++++++++++++++++ plugins/inputs/mock_Plugin.go | 39 -- 4 files changed, 575 insertions(+), 39 deletions(-) rename plugins/inputs/{EXAMPLE_README.md => example/README.md} (100%) create mode 100644 plugins/inputs/example/example.go create mode 100644 plugins/inputs/example/example_test.go delete mode 100644 plugins/inputs/mock_Plugin.go diff --git a/plugins/inputs/EXAMPLE_README.md b/plugins/inputs/example/README.md similarity index 100% rename from plugins/inputs/EXAMPLE_README.md rename to plugins/inputs/example/README.md diff --git a/plugins/inputs/example/example.go b/plugins/inputs/example/example.go new file mode 100644 index 000000000..c8f5992fe --- /dev/null +++ b/plugins/inputs/example/example.go @@ -0,0 +1,136 @@ +package example + +import ( + "fmt" + "math/rand" + "time" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/config" + "github.com/influxdata/telegraf/plugins/inputs" +) + +// Example struct should be named the same as the Plugin +type Example struct { + // Example for a mandatory option to set a tag + DeviceName string `toml:"device_name"` + + // Config options are converted to the correct type automatically + NumberFields int64 `toml:"number_fields"` + + // We can also use booleans and have diverging names between user-configuration options and struct members + EnableRandomVariable bool `toml:"enable_random"` + + // Example of passing a duration option allowing the format of e.g. "100ms", "5m" or "1h" + Timeout config.Duration `toml:"timeout"` + + // Telegraf logging facility + // The exact name is important to allow automatic initialization by telegraf. + Log telegraf.Logger `toml:"-"` + + // This is a non-exported internal state. + count int64 +} + +// Usually the default (example) configuration is contained in this constant. +// Please use '## '' to denote comments and '# ' to specify default settings and start each line with two spaces. +const sampleConfig = ` + ## Device name used as a tag + ## This is a mandatory option that needs to be set by the user, so we do not + ## comment it. + device_name = "" + + ## Number of fields contained in the output + ## This should be greater than zero and less then ten. + ## Here, two is the default, so we comment the option with the default value shown. + # number_fields = 2 + + ## Enable setting the field(s) to random values + ## By default, the field values are set to zero. + # enable_random = false + + ## Specify a duration allowing time-unit suffixes ('ns','ms', 's', 'm', etc.) + # timeout = "100ms" +` + +// Description will appear directly above the plugin definition in the config file +func (m *Example) Description() string { + return `This is an example plugin` +} + +// SampleConfig will populate the sample configuration portion of the plugin's configuration +func (m *Example) SampleConfig() string { + return sampleConfig +} + +// Init can be implemented to do one-time processing stuff like initializing variables +func (m *Example) Init() error { + // Check your options according to your requirements + if m.DeviceName == "" { + return fmt.Errorf("device name cannot be empty") + } + + // Set your defaults. + // Please note: In golang all fields are initialzed to their nil value, so you should not + // set these fields if the nil value is what you want (e.g. for booleans). + if m.NumberFields < 1 { + m.Log.Debugf("Setting number of fields to default from invalid value %d", m.NumberFields) + m.NumberFields = 2 + } + + // Initialze your internal states + m.count = 1 + + return nil +} + +// Gather defines what data the plugin will gather. +func (m *Example) Gather(acc telegraf.Accumulator) error { + // Imagine some completely arbitrary error occuring here + if m.NumberFields > 10 { + return fmt.Errorf("too many fields") + } + + // For illustration we gather three metrics in one go + for run := 0; run < 3; run++ { + // Imagine an error occurs here but you want to keep the other + // metrics, then you cannot simply return, as this would drop + // all later metrics. Simply accumulate errors in this case + // and ignore the metric. + if m.EnableRandomVariable && m.DeviceName == "flappy" && run > 1 { + acc.AddError(fmt.Errorf("too many runs for random values")) + continue + } + + // Construct the fields + fields := map[string]interface{}{"count": m.count} + for i := int64(1); i < m.NumberFields; i++ { + name := fmt.Sprintf("field%d", i) + value := 0.0 + if m.EnableRandomVariable { + value = rand.Float64() + } + fields[name] = value + } + + // Construct the tags + tags := map[string]string{"device": m.DeviceName} + + // Add the metric with the current timestamp + acc.AddFields("example", fields, tags) + + m.count++ + } + + return nil +} + +// Register the plugin +func init() { + inputs.Add("example", func() telegraf.Input { + return &Example{ + // Set the default timeout here to distinguish it from the user setting it to zero + Timeout: config.Duration(100 * time.Millisecond), + } + }) +} diff --git a/plugins/inputs/example/example_test.go b/plugins/inputs/example/example_test.go new file mode 100644 index 000000000..1c3b4b0a5 --- /dev/null +++ b/plugins/inputs/example/example_test.go @@ -0,0 +1,439 @@ +package example + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/config" + "github.com/influxdata/telegraf/testutil" +) + +// This file should contain a set of unit-tests to cover your plugin. This will ease +// spotting bugs and mistakes when later modifying or extending the functionality. +// To do so, please write one 'TestXYZ' function per 'case' e.g. default init, +// things that should fail or expected values from a mockup. + +func TestInitDefault(t *testing.T) { + // This test should succeed with the default initialization. + + // Use whatever you use in the init() function plus the mandatory options. + // ATTENTION: Always initialze the "Log" as you will get SIGSEGV otherwise. + plugin := &Example{ + DeviceName: "test", + Timeout: config.Duration(100 * time.Millisecond), + Log: testutil.Logger{}, + } + + // Test the initialization succeeds + require.NoError(t, plugin.Init()) + + // Also test that default values are set correctly + require.Equal(t, config.Duration(100*time.Millisecond), plugin.Timeout) + require.Equal(t, "test", plugin.DeviceName) + require.Equal(t, int64(2), plugin.NumberFields) +} + +func TestInitFail(t *testing.T) { + // You should also test for your safety nets to work i.e. you get errors for + // invalid configuration-option values. So check your error paths in Init() + // and check if you reach them + + // We setup a table-test here to specify "setting" - "expected error" values. + // Eventhough it seems overkill here for the example plugin, we reuse this structure + // later for checking the metrics + tests := []struct { + name string + plugin *Example + expected string + }{ + { + name: "all empty", + plugin: &Example{}, + expected: "device name cannot be empty", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Always initialze the logger to avoid SIGSEGV. This is done automatically by + // telegraf during normal operation. + tt.plugin.Log = testutil.Logger{} + err := tt.plugin.Init() + require.Error(t, err) + require.EqualError(t, err, tt.expected) + }) + } +} + +func TestFixedValue(t *testing.T) { + // You can organize the test e.g. by operation mode (like we do here random vs. fixed), by features or + // by different metrics gathered. Please choose the partitioning most suited for your plugin + + // We again setup a table-test here to specify "setting" - "expected output metric" pairs. + tests := []struct { + name string + plugin *Example + expected []telegraf.Metric + }{ + { + name: "count only", + plugin: &Example{ + DeviceName: "test", + NumberFields: 1, + }, + expected: []telegraf.Metric{ + testutil.MustMetric( + "example", + map[string]string{ + "device": "test", + }, + map[string]interface{}{ + "count": 1, + }, + time.Unix(0, 0), + ), + testutil.MustMetric( + "example", + map[string]string{ + "device": "test", + }, + map[string]interface{}{ + "count": 2, + }, + time.Unix(0, 0), + ), + testutil.MustMetric( + "example", + map[string]string{ + "device": "test", + }, + map[string]interface{}{ + "count": 3, + }, + time.Unix(0, 0), + ), + }, + }, + { + name: "default settings", + plugin: &Example{ + DeviceName: "test", + }, + expected: []telegraf.Metric{ + testutil.MustMetric( + "example", + map[string]string{ + "device": "test", + }, + map[string]interface{}{ + "count": 1, + "field1": float64(0), + }, + time.Unix(0, 0), + ), + testutil.MustMetric( + "example", + map[string]string{ + "device": "test", + }, + map[string]interface{}{ + "count": 2, + "field1": float64(0), + }, + time.Unix(0, 0), + ), + testutil.MustMetric( + "example", + map[string]string{ + "device": "test", + }, + map[string]interface{}{ + "count": 3, + "field1": float64(0), + }, + time.Unix(0, 0), + ), + }, + }, + { + name: "more fields", + plugin: &Example{ + DeviceName: "test", + NumberFields: 4, + }, + expected: []telegraf.Metric{ + testutil.MustMetric( + "example", + map[string]string{ + "device": "test", + }, + map[string]interface{}{ + "count": 1, + "field1": float64(0), + "field2": float64(0), + "field3": float64(0), + }, + time.Unix(0, 0), + ), + testutil.MustMetric( + "example", + map[string]string{ + "device": "test", + }, + map[string]interface{}{ + "count": 2, + "field1": float64(0), + "field2": float64(0), + "field3": float64(0), + }, + time.Unix(0, 0), + ), + testutil.MustMetric( + "example", + map[string]string{ + "device": "test", + }, + map[string]interface{}{ + "count": 3, + "field1": float64(0), + "field2": float64(0), + "field3": float64(0), + }, + time.Unix(0, 0), + ), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var acc testutil.Accumulator + + tt.plugin.Log = testutil.Logger{} + require.NoError(t, tt.plugin.Init()) + + // Call gather and check no error occurs. In case you use acc.AddError() somewhere + // in your code, it is not sufficient to only check the return value of Gather(). + require.NoError(t, tt.plugin.Gather(&acc)) + require.Len(t, acc.Errors, 0, "found errors accumulated by acc.AddError()") + + // Wait for the expected number of metrics to avoid flaky tests due to + // race conditions. + acc.Wait(len(tt.expected)) + + // Compare the metrics in a convenient way. Here we ignore + // the metric time during comparision as we cannot inject the time + // during test. For more comparision options check testutil package. + testutil.RequireMetricsEqual(t, tt.expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime()) + }) + } +} + +func TestRandomValue(t *testing.T) { + // Sometimes, you cannot know the exact outcome of the gather cycle e.g. if the gathering involves random data. + // However, you should check the result nevertheless, applying as many conditions as you can. + + // We again setup a table-test here to specify "setting" - "expected output metric" pairs. + tests := []struct { + name string + plugin *Example + template telegraf.Metric + }{ + { + name: "count only", + plugin: &Example{ + DeviceName: "test", + NumberFields: 1, + EnableRandomVariable: true, + }, + template: testutil.MustMetric( + "example", + map[string]string{ + "device": "test", + }, + map[string]interface{}{ + "count": 1, + }, + time.Unix(0, 0), + ), + }, + { + name: "default settings", + plugin: &Example{ + DeviceName: "test", + EnableRandomVariable: true, + }, + template: testutil.MustMetric( + "example", + map[string]string{ + "device": "test", + }, + map[string]interface{}{ + "count": 1, + "field1": float64(0), + }, + time.Unix(0, 0), + ), + }, + { + name: "more fields", + plugin: &Example{ + DeviceName: "test", + NumberFields: 4, + EnableRandomVariable: true, + }, + template: testutil.MustMetric( + "example", + map[string]string{ + "device": "test", + }, + map[string]interface{}{ + "count": 1, + "field1": float64(0), + "field2": float64(0), + "field3": float64(0), + }, + time.Unix(0, 0), + ), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var acc testutil.Accumulator + + tt.plugin.Log = testutil.Logger{} + require.NoError(t, tt.plugin.Init()) + + // Call gather and check no error occurs. In case you use acc.AddError() somewhere + // in your code, it is not sufficient to only check the return value of Gather(). + require.NoError(t, tt.plugin.Gather(&acc)) + require.Len(t, acc.Errors, 0, "found errors accumulated by acc.AddError()") + + // Wait for the expected number of metrics to avoid flaky tests due to + // race conditions. + acc.Wait(3) + + // Compare all aspects of the metric that are known to you + for i, m := range acc.GetTelegrafMetrics() { + require.Equal(t, m.Name(), tt.template.Name()) + require.Equal(t, m.Tags(), tt.template.Tags()) + + // Check if all expected fields are there + fields := m.Fields() + for k := range tt.template.Fields() { + if k == "count" { + require.Equal(t, fields["count"], int64(i+1)) + continue + } + _, found := fields[k] + require.Truef(t, found, "field %q not found", k) + } + } + }) + } +} + +func TestGatherFail(t *testing.T) { + // You should also test for error conditions in your Gather() method. Try to cover all error paths. + + // We again setup a table-test here to specify "setting" - "expected error" pair. + tests := []struct { + name string + plugin *Example + expected string + }{ + { + name: "too many fields", + plugin: &Example{ + DeviceName: "test", + NumberFields: 11, + }, + expected: "too many fields", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var acc testutil.Accumulator + + tt.plugin.Log = testutil.Logger{} + require.NoError(t, tt.plugin.Init()) + + err := tt.plugin.Gather(&acc) + require.Error(t, err) + require.EqualError(t, err, tt.expected) + }) + } +} + +func TestRandomValueFailPartial(t *testing.T) { + // You should also test for error conditions in your Gather() with partial output. This is required when + // using acc.AddError() as Gather() might succeed (return nil) but there are some metrics missing. + + // We again setup a table-test here to specify "setting" - "expected output metric" and "errors". + tests := []struct { + name string + plugin *Example + expected []telegraf.Metric + expectedErr string + }{ + { + name: "flappy gather", + plugin: &Example{ + DeviceName: "flappy", + NumberFields: 1, + EnableRandomVariable: true, + }, + expected: []telegraf.Metric{ + testutil.MustMetric( + "example", + map[string]string{ + "device": "flappy", + }, + map[string]interface{}{ + "count": 1, + }, + time.Unix(0, 0), + ), + testutil.MustMetric( + "example", + map[string]string{ + "device": "flappy", + }, + map[string]interface{}{ + "count": 2, + }, + time.Unix(0, 0), + ), + }, + expectedErr: "too many runs for random values", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var acc testutil.Accumulator + + tt.plugin.Log = testutil.Logger{} + require.NoError(t, tt.plugin.Init()) + + // Call gather and check no error occurs. However, we expect an error accumulated by acc.AddError() + require.NoError(t, tt.plugin.Gather(&acc)) + + // Wait for the expected number of metrics to avoid flaky tests due to + // race conditions. + acc.Wait(len(tt.expected)) + + // Check the accumulated errors + require.Len(t, acc.Errors, 1) + require.EqualError(t, acc.Errors[0], tt.expectedErr) + + // Compare the expected partial metrics. + testutil.RequireMetricsEqual(t, tt.expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime()) + }) + } +} diff --git a/plugins/inputs/mock_Plugin.go b/plugins/inputs/mock_Plugin.go deleted file mode 100644 index 7270954dc..000000000 --- a/plugins/inputs/mock_Plugin.go +++ /dev/null @@ -1,39 +0,0 @@ -package inputs - -import ( - "github.com/influxdata/telegraf" - - "github.com/stretchr/testify/mock" -) - -// MockPlugin struct should be named the same as the Plugin -type MockPlugin struct { - mock.Mock - - constructedVariable string -} - -// Description will appear directly above the plugin definition in the config file -func (m *MockPlugin) Description() string { - return `This is an example plugin` -} - -// SampleConfig will populate the sample configuration portion of the plugin's configuration -func (m *MockPlugin) SampleConfig() string { - return ` sampleVar = 'foo'` -} - -// Init can be implemented to do one-time processing stuff like initializing variables -func (m *MockPlugin) Init() error { - m.constructedVariable = "I'm initialized now." - return nil -} - -// Gather defines what data the plugin will gather. -func (m *MockPlugin) Gather(_a0 telegraf.Accumulator) error { - ret := m.Called(_a0) - - r0 := ret.Error(0) - - return r0 -}