forked from influxdata/telegraf
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add exec output plugin (influxdata#6267)
- Loading branch information
1 parent
1ad10c8
commit 819bf8e
Showing
6 changed files
with
291 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# Exec Output Plugin | ||
|
||
This plugin sends telegraf metrics to an external application over stdin. | ||
|
||
The command should be defined similar to docker's `exec` form: | ||
|
||
["executable", "param1", "param2"] | ||
|
||
On non-zero exit stderr will be logged at error level. | ||
|
||
### Configuration | ||
|
||
```toml | ||
[[outputs.exec]] | ||
## Command to injest metrics via stdin. | ||
command = ["tee", "-a", "/dev/null"] | ||
|
||
## Timeout for command to complete. | ||
# timeout = "5s" | ||
|
||
## Data format to output. | ||
## Each data format has its own unique set of configuration options, read | ||
## more about them here: | ||
## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_OUTPUT.md | ||
# data_format = "influx" | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
package exec | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"log" | ||
"os/exec" | ||
"time" | ||
|
||
"github.com/influxdata/telegraf" | ||
"github.com/influxdata/telegraf/internal" | ||
"github.com/influxdata/telegraf/plugins/outputs" | ||
"github.com/influxdata/telegraf/plugins/serializers" | ||
) | ||
|
||
const maxStderrBytes = 512 | ||
|
||
// Exec defines the exec output plugin. | ||
type Exec struct { | ||
Command []string `toml:"command"` | ||
Timeout internal.Duration `toml:"timeout"` | ||
|
||
runner Runner | ||
serializer serializers.Serializer | ||
} | ||
|
||
var sampleConfig = ` | ||
## Command to injest metrics via stdin. | ||
command = ["tee", "-a", "/dev/null"] | ||
## Timeout for command to complete. | ||
# timeout = "5s" | ||
## Data format to output. | ||
## Each data format has its own unique set of configuration options, read | ||
## more about them here: | ||
## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_OUTPUT.md | ||
# data_format = "influx" | ||
` | ||
|
||
// SetSerializer sets the serializer for the output. | ||
func (e *Exec) SetSerializer(serializer serializers.Serializer) { | ||
e.serializer = serializer | ||
} | ||
|
||
// Connect satisfies the Ouput interface. | ||
func (e *Exec) Connect() error { | ||
return nil | ||
} | ||
|
||
// Close satisfies the Ouput interface. | ||
func (e *Exec) Close() error { | ||
return nil | ||
} | ||
|
||
// Description describes the plugin. | ||
func (e *Exec) Description() string { | ||
return "Send metrics to command as input over stdin" | ||
} | ||
|
||
// SampleConfig returns a sample configuration. | ||
func (e *Exec) SampleConfig() string { | ||
return sampleConfig | ||
} | ||
|
||
// Write writes the metrics to the configured command. | ||
func (e *Exec) Write(metrics []telegraf.Metric) error { | ||
var buffer bytes.Buffer | ||
for _, metric := range metrics { | ||
value, err := e.serializer.Serialize(metric) | ||
if err != nil { | ||
return err | ||
} | ||
buffer.Write(value) | ||
} | ||
|
||
if buffer.Len() <= 0 { | ||
return nil | ||
} | ||
|
||
return e.runner.Run(e.Timeout.Duration, e.Command, &buffer) | ||
} | ||
|
||
// Runner provides an interface for running exec.Cmd. | ||
type Runner interface { | ||
Run(time.Duration, []string, io.Reader) error | ||
} | ||
|
||
// CommandRunner runs a command with the ability to kill the process before the timeout. | ||
type CommandRunner struct { | ||
cmd *exec.Cmd | ||
} | ||
|
||
// Run runs the command. | ||
func (c *CommandRunner) Run(timeout time.Duration, command []string, buffer io.Reader) error { | ||
cmd := exec.Command(command[0], command[1:]...) | ||
cmd.Stdin = buffer | ||
var stderr bytes.Buffer | ||
cmd.Stderr = &stderr | ||
|
||
err := internal.RunTimeout(cmd, timeout) | ||
s := stderr | ||
|
||
if err != nil { | ||
if err == internal.TimeoutErr { | ||
return fmt.Errorf("%q timed out and was killed", command) | ||
} | ||
|
||
if s.Len() > 0 { | ||
log.Printf("E! [outputs.exec] Command error: %q", truncate(s)) | ||
} | ||
|
||
if status, ok := internal.ExitStatus(err); ok { | ||
return fmt.Errorf("%q exited %d with %s", command, status, err.Error()) | ||
} | ||
|
||
return fmt.Errorf("%q failed with %s", command, err.Error()) | ||
} | ||
|
||
c.cmd = cmd | ||
|
||
return nil | ||
} | ||
|
||
func truncate(buf bytes.Buffer) string { | ||
// Limit the number of bytes. | ||
didTruncate := false | ||
if buf.Len() > maxStderrBytes { | ||
buf.Truncate(maxStderrBytes) | ||
didTruncate = true | ||
} | ||
if i := bytes.IndexByte(buf.Bytes(), '\n'); i > 0 { | ||
// Only show truncation if the newline wasn't the last character. | ||
if i < buf.Len()-1 { | ||
didTruncate = true | ||
} | ||
buf.Truncate(i) | ||
} | ||
if didTruncate { | ||
buf.WriteString("...") | ||
} | ||
return buf.String() | ||
} | ||
|
||
func init() { | ||
outputs.Add("exec", func() telegraf.Output { | ||
return &Exec{ | ||
runner: &CommandRunner{}, | ||
Timeout: internal.Duration{Duration: time.Second * 5}, | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package exec | ||
|
||
import ( | ||
"bytes" | ||
"strings" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/influxdata/telegraf" | ||
"github.com/influxdata/telegraf/internal" | ||
"github.com/influxdata/telegraf/plugins/serializers" | ||
"github.com/influxdata/telegraf/testutil" | ||
) | ||
|
||
func TestExec(t *testing.T) { | ||
if testing.Short() { | ||
t.Skip("Skipping test due to OS/executable dependencies") | ||
} | ||
|
||
tests := []struct { | ||
name string | ||
command []string | ||
err bool | ||
metrics []telegraf.Metric | ||
}{ | ||
{ | ||
name: "test success", | ||
command: []string{"tee"}, | ||
err: false, | ||
metrics: testutil.MockMetrics(), | ||
}, | ||
{ | ||
name: "test doesn't accept stdin", | ||
command: []string{"sleep", "5s"}, | ||
err: true, | ||
metrics: testutil.MockMetrics(), | ||
}, | ||
{ | ||
name: "test command not found", | ||
command: []string{"/no/exist", "-h"}, | ||
err: true, | ||
metrics: testutil.MockMetrics(), | ||
}, | ||
{ | ||
name: "test no metrics output", | ||
command: []string{"tee"}, | ||
err: false, | ||
metrics: []telegraf.Metric{}, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
e := &Exec{ | ||
Command: tt.command, | ||
Timeout: internal.Duration{Duration: time.Second}, | ||
runner: &CommandRunner{}, | ||
} | ||
|
||
s, _ := serializers.NewInfluxSerializer() | ||
e.SetSerializer(s) | ||
|
||
e.Connect() | ||
|
||
require.Equal(t, tt.err, e.Write(tt.metrics) != nil) | ||
}) | ||
} | ||
} | ||
|
||
func TestTruncate(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
buf *bytes.Buffer | ||
len int | ||
}{ | ||
{ | ||
name: "long out", | ||
buf: bytes.NewBufferString(strings.Repeat("a", maxStderrBytes+100)), | ||
len: maxStderrBytes + len("..."), | ||
}, | ||
{ | ||
name: "multiline out", | ||
buf: bytes.NewBufferString("hola\ngato\n"), | ||
len: len("hola") + len("..."), | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
s := truncate(*tt.buf) | ||
require.Equal(t, tt.len, len(s)) | ||
}) | ||
} | ||
} | ||
|
||
func TestExecDocs(t *testing.T) { | ||
e := &Exec{} | ||
e.Description() | ||
e.SampleConfig() | ||
require.NoError(t, e.Close()) | ||
|
||
e = &Exec{runner: &CommandRunner{}} | ||
require.NoError(t, e.Close()) | ||
} |