1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,100 @@
# Execd Input Plugin
This plugin runs the given external program as a long-running daemon and collects
the metrics in one of the supported [data formats][data_formats] on the
process's `stdout`. The program is expected to stay running and output data
when receiving the configured `signal`.
The `stderr` output of the process will be relayed to Telegraf's logging
facilities and will be logged as _error_ by default. However, you can log to
other levels by prefixing your message with `E!` for error, `W!` for warning,
`I!` for info, `D!` for debugging and `T!` for trace levels followed by a space
and the actual message. For example outputting `I! A log message` will create a
`info` log line in your Telegraf logging output.
⭐ Telegraf v1.14.0
🏷️ system
💻 all
[data_formats]: /docs/DATA_FORMATS_INPUT.md
## Service Input <!-- @/docs/includes/service_input.md -->
This plugin is a service input. Normal plugins gather metrics determined by the
interval setting. Service plugins start a service to listen and wait for
metrics or events to occur. Service plugins have two key differences from
normal plugins:
1. The global or plugin specific `interval` setting may not apply
2. The CLI options of `--test`, `--test-wait`, and `--once` may not produce
output for this plugin
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
## Configuration
```toml @sample.conf
# Run executable as long-running input plugin
[[inputs.execd]]
## One program to run as daemon.
## NOTE: process and each argument should each be their own string
command = ["telegraf-smartctl", "-d", "/dev/sda"]
## Environment variables
## Array of "key=value" pairs to pass as environment variables
## e.g. "KEY=value", "USERNAME=John Doe",
## "LD_LIBRARY_PATH=/opt/custom/lib64:/usr/local/libs"
# environment = []
## Define how the process is signaled on each collection interval.
## Valid values are:
## "none" : Do not signal anything. (Recommended for service inputs)
## The process must output metrics by itself.
## "STDIN" : Send a newline on STDIN. (Recommended for gather inputs)
## "SIGHUP" : Send a HUP signal. Not available on Windows. (not recommended)
## "SIGUSR1" : Send a USR1 signal. Not available on Windows.
## "SIGUSR2" : Send a USR2 signal. Not available on Windows.
# signal = "none"
## Delay before the process is restarted after an unexpected termination
# restart_delay = "10s"
## Buffer size used to read from the command output stream
## Optional parameter. Default is 64 Kib, minimum is 16 bytes
# buffer_size = "64Kib"
## Disable automatic restart of the program and stop if the program exits
## with an error (i.e. non-zero error code)
# stop_on_error = false
## Data format to consume.
## 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_INPUT.md
# data_format = "influx"
```
## Example
See the examples directory for basic examples in different languages expecting
various signals from Telegraf:
- [Go](./examples/count.go): Example expects `signal = "SIGHUP"`
- [Python](./examples/count.py): Example expects `signal = "none"`
- [Ruby](./examples/count.rb): Example expects `signal = "none"`
- [shell](./examples/count.sh): Example expects `signal = "STDIN"`
## Metrics
Varies depending on the users data.
## Example Output
Varies depending on the users data.

View file

@ -0,0 +1,24 @@
package main
// Example using HUP signaling
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP)
counter := 0
for {
<-c
fmt.Printf("counter_go count=%d\n", counter)
counter++
}
}

View file

@ -0,0 +1,12 @@
#!/usr/bin/env python3
import sys
import time
COUNTER = 0
while True:
print("counter_python count=" + str(COUNTER))
sys.stdout.flush()
COUNTER += 1
time.sleep(1)

View file

@ -0,0 +1,21 @@
#!/usr/bin/env ruby
## Example in Ruby not using any signaling
counter = 0
def time_ns_str(t)
ns = t.nsec.to_s
(9 - ns.size).times do
ns = "0" + ns # left pad
end
t.to_i.to_s + ns
end
loop do
puts "counter_ruby count=#{counter} #{time_ns_str(Time.now)}"
STDOUT.flush
counter += 1
sleep 1
end

View file

@ -0,0 +1,12 @@
#!/bin/sh
## Example in bash using STDIN signaling
counter=0
while read -r _; do
echo "counter_bash count=${counter}"
counter=$((counter+1))
done
trap "echo terminate 1>&2" EXIT

View file

@ -0,0 +1,186 @@
//go:generate ../../../tools/readme_config_includer/generator
package execd
import (
"bufio"
_ "embed"
"errors"
"fmt"
"io"
"os"
"strings"
"sync"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/internal/process"
"github.com/influxdata/telegraf/models"
"github.com/influxdata/telegraf/plugins/inputs"
"github.com/influxdata/telegraf/plugins/parsers/influx"
)
//go:embed sample.conf
var sampleConfig string
var once sync.Once
type Execd struct {
Command []string `toml:"command"`
Environment []string `toml:"environment"`
BufferSize config.Size `toml:"buffer_size"`
Signal string `toml:"signal"`
RestartDelay config.Duration `toml:"restart_delay"`
StopOnError bool `toml:"stop_on_error"`
Log telegraf.Logger `toml:"-"`
process *process.Process
acc telegraf.Accumulator
parser telegraf.Parser
outputReader func(io.Reader)
}
func (*Execd) SampleConfig() string {
return sampleConfig
}
func (e *Execd) Init() error {
if len(e.Command) == 0 {
return errors.New("no command specified")
}
return nil
}
func (e *Execd) SetParser(parser telegraf.Parser) {
e.parser = parser
e.outputReader = e.cmdReadOut
unwrapped, ok := parser.(*models.RunningParser)
if ok {
if _, ok := unwrapped.Parser.(*influx.Parser); ok {
e.outputReader = e.cmdReadOutStream
}
}
}
func (e *Execd) Start(acc telegraf.Accumulator) error {
e.acc = acc
var err error
e.process, err = process.New(e.Command, e.Environment)
if err != nil {
return fmt.Errorf("error creating new process: %w", err)
}
e.process.ReadStdoutFn = e.outputReader
e.process.ReadStderrFn = e.cmdReadErr
e.process.RestartDelay = time.Duration(e.RestartDelay)
e.process.StopOnError = e.StopOnError
e.process.Log = e.Log
if err = e.process.Start(); err != nil {
// if there was only one argument, and it contained spaces, warn the user
// that they may have configured it wrong.
if len(e.Command) == 1 && strings.Contains(e.Command[0], " ") {
e.Log.Warn("The inputs.execd Command contained spaces but no arguments. " +
"This setting expects the program and arguments as an array of strings, " +
"not as a space-delimited string. See the plugin readme for an example.")
}
return fmt.Errorf("failed to start process %s: %w", e.Command, err)
}
return nil
}
func (e *Execd) Stop() {
e.process.Stop()
}
func (e *Execd) cmdReadOut(out io.Reader) {
rdr := bufio.NewReaderSize(out, int(e.BufferSize))
for {
data, err := rdr.ReadBytes('\n')
if err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, os.ErrClosed) {
break
}
e.acc.AddError(fmt.Errorf("error reading stdout: %w", err))
continue
}
metrics, err := e.parser.Parse(data)
if err != nil {
e.acc.AddError(fmt.Errorf("parse error: %w", err))
}
if len(metrics) == 0 {
once.Do(func() {
e.Log.Debug(internal.NoMetricsCreatedMsg)
})
}
for _, metric := range metrics {
e.acc.AddMetric(metric)
}
}
}
func (e *Execd) cmdReadOutStream(out io.Reader) {
parser := influx.NewStreamParser(out)
for {
metric, err := parser.Next()
if err != nil {
if errors.Is(err, influx.EOF) {
break // stream ended
}
var parseErr *influx.ParseError
if errors.As(err, &parseErr) {
// parse error.
e.acc.AddError(parseErr)
continue
}
// some non-recoverable error?
e.acc.AddError(err)
return
}
e.acc.AddMetric(metric)
}
}
func (e *Execd) cmdReadErr(out io.Reader) {
scanner := bufio.NewScanner(out)
for scanner.Scan() {
msg := scanner.Text()
switch {
case strings.HasPrefix(msg, "E! "):
e.Log.Error(msg[3:])
case strings.HasPrefix(msg, "W! "):
e.Log.Warn(msg[3:])
case strings.HasPrefix(msg, "I! "):
e.Log.Info(msg[3:])
case strings.HasPrefix(msg, "D! "):
e.Log.Debug(msg[3:])
case strings.HasPrefix(msg, "T! "):
e.Log.Trace(msg[3:])
default:
e.Log.Errorf("stderr: %q", msg)
}
}
if err := scanner.Err(); err != nil {
e.acc.AddError(fmt.Errorf("error reading stderr: %w", err))
}
}
func init() {
inputs.Add("execd", func() telegraf.Input {
return &Execd{
Signal: "none",
RestartDelay: config.Duration(10 * time.Second),
BufferSize: config.Size(64 * 1024),
}
})
}

View file

@ -0,0 +1,46 @@
//go:build !windows
package execd
import (
"fmt"
"io"
"os"
"syscall"
"time"
"github.com/influxdata/telegraf"
)
func (e *Execd) Gather(_ telegraf.Accumulator) error {
if e.process == nil || e.process.Cmd == nil {
return nil
}
osProcess := e.process.Cmd.Process
if osProcess == nil {
return nil
}
switch e.Signal {
case "SIGHUP":
return osProcess.Signal(syscall.SIGHUP)
case "SIGUSR1":
return osProcess.Signal(syscall.SIGUSR1)
case "SIGUSR2":
return osProcess.Signal(syscall.SIGUSR2)
case "STDIN":
if osStdin, ok := e.process.Stdin.(*os.File); ok {
if err := osStdin.SetWriteDeadline(time.Now().Add(1 * time.Second)); err != nil {
return fmt.Errorf("setting write deadline failed: %w", err)
}
}
if _, err := io.WriteString(e.process.Stdin, "\n"); err != nil {
return fmt.Errorf("writing to stdin failed: %w", err)
}
case "none":
default:
return fmt.Errorf("invalid signal: %s", e.Signal)
}
return nil
}

View file

@ -0,0 +1,459 @@
package execd
import (
"bufio"
"flag"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/agent"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/logger"
"github.com/influxdata/telegraf/metric"
"github.com/influxdata/telegraf/models"
"github.com/influxdata/telegraf/plugins/parsers/influx"
"github.com/influxdata/telegraf/plugins/parsers/prometheus"
serializers_influx "github.com/influxdata/telegraf/plugins/serializers/influx"
"github.com/influxdata/telegraf/testutil"
)
func TestSettingConfigWorks(t *testing.T) {
cfg := `
[[inputs.execd]]
command = ["a", "b", "c"]
environment = ["d=e", "f=1"]
restart_delay = "1m"
signal = "SIGHUP"
`
conf := config.NewConfig()
require.NoError(t, conf.LoadConfigData([]byte(cfg), config.EmptySourcePath))
require.Len(t, conf.Inputs, 1)
inp, ok := conf.Inputs[0].Input.(*Execd)
require.True(t, ok)
require.EqualValues(t, []string{"a", "b", "c"}, inp.Command)
require.EqualValues(t, []string{"d=e", "f=1"}, inp.Environment)
require.EqualValues(t, 1*time.Minute, inp.RestartDelay)
require.EqualValues(t, "SIGHUP", inp.Signal)
}
func TestExternalInputWorks(t *testing.T) {
influxParser := models.NewRunningParser(&influx.Parser{}, &models.ParserConfig{})
require.NoError(t, influxParser.Init())
exe, err := os.Executable()
require.NoError(t, err)
e := &Execd{
Command: []string{exe, "-mode", "counter"},
Environment: []string{"PLUGINS_INPUTS_EXECD_MODE=application", "METRIC_NAME=counter"},
RestartDelay: config.Duration(5 * time.Second),
Signal: "STDIN",
Log: testutil.Logger{},
}
e.SetParser(influxParser)
metrics := make(chan telegraf.Metric, 10)
defer close(metrics)
acc := agent.NewAccumulator(&TestMetricMaker{}, metrics)
require.NoError(t, e.Start(acc))
require.NoError(t, e.Gather(acc))
// grab a metric and make sure it's a thing
m := readChanWithTimeout(t, metrics, 10*time.Second)
e.Stop()
require.Equal(t, "counter", m.Name())
val, ok := m.GetField("count")
require.True(t, ok)
require.EqualValues(t, 0, val)
}
func TestParsesLinesContainingNewline(t *testing.T) {
parser := models.NewRunningParser(&influx.Parser{}, &models.ParserConfig{})
require.NoError(t, parser.Init())
metrics := make(chan telegraf.Metric, 10)
defer close(metrics)
acc := agent.NewAccumulator(&TestMetricMaker{}, metrics)
e := &Execd{
RestartDelay: config.Duration(5 * time.Second),
Signal: "STDIN",
acc: acc,
Log: testutil.Logger{},
}
e.SetParser(parser)
cases := []struct {
Name string
Value string
}{
{
Name: "no-newline",
Value: "my message",
}, {
Name: "newline",
Value: "my\nmessage",
},
}
for _, test := range cases {
t.Run(test.Name, func(t *testing.T) {
line := fmt.Sprintf("event message=\"%v\" 1587128639239000000", test.Value)
e.outputReader(strings.NewReader(line))
m := readChanWithTimeout(t, metrics, 1*time.Second)
require.Equal(t, "event", m.Name())
val, ok := m.GetField("message")
require.True(t, ok)
require.Equal(t, test.Value, val)
})
}
}
func TestParsesPrometheus(t *testing.T) {
parser := models.NewRunningParser(&prometheus.Parser{}, &models.ParserConfig{})
require.NoError(t, parser.Init())
metrics := make(chan telegraf.Metric, 10)
defer close(metrics)
var acc testutil.Accumulator
e := &Execd{
RestartDelay: config.Duration(5 * time.Second),
Signal: "STDIN",
acc: &acc,
Log: testutil.Logger{},
}
e.SetParser(parser)
lines := `# HELP This is just a test metric.
# TYPE test summary
test{handler="execd",quantile="0.5"} 42.0
`
expected := []telegraf.Metric{
testutil.MustMetric(
"prometheus",
map[string]string{"handler": "execd", "quantile": "0.5"},
map[string]interface{}{"test": float64(42.0)},
time.Unix(0, 0),
),
}
e.outputReader(strings.NewReader(lines))
check := func() bool { return acc.NMetrics() == uint64(len(expected)) }
require.Eventually(t, check, 1*time.Second, 100*time.Millisecond)
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime())
}
func TestStopOnError(t *testing.T) {
exe, err := os.Executable()
require.NoError(t, err)
plugin := &Execd{
Command: []string{exe, "-mode", "fail"},
Environment: []string{"PLUGINS_INPUTS_EXECD_MODE=application"},
StopOnError: true,
RestartDelay: config.Duration(5 * time.Second),
Log: testutil.Logger{},
}
parser := models.NewRunningParser(&influx.Parser{}, &models.ParserConfig{})
require.NoError(t, parser.Init())
plugin.SetParser(parser)
var acc testutil.Accumulator
require.NoError(t, plugin.Start(&acc))
defer plugin.Stop()
require.Eventually(t, func() bool {
_, running := plugin.process.State()
return !running
}, 3*time.Second, 100*time.Millisecond)
state, running := plugin.process.State()
require.False(t, running)
require.Equal(t, 42, state.ExitCode())
}
func TestStopOnErrorSuccess(t *testing.T) {
exe, err := os.Executable()
require.NoError(t, err)
plugin := &Execd{
Command: []string{exe, "-mode", "success"},
Environment: []string{"PLUGINS_INPUTS_EXECD_MODE=application"},
StopOnError: true,
RestartDelay: config.Duration(100 * time.Millisecond),
Log: testutil.Logger{},
}
parser := models.NewRunningParser(&influx.Parser{}, &models.ParserConfig{})
require.NoError(t, parser.Init())
plugin.SetParser(parser)
var acc testutil.Accumulator
require.NoError(t, plugin.Start(&acc))
defer plugin.Stop()
// Wait for at least two metric as this indicates the process was restarted
require.Eventually(t, func() bool {
return acc.NMetrics() > 1
}, 3*time.Second, 100*time.Millisecond)
}
func TestLoggingNoPrefix(t *testing.T) {
// Use own test as mocking executable
exe, err := os.Executable()
require.NoError(t, err)
// Setup the plugin with a capturing logger
var l testutil.CaptureLogger
plugin := &Execd{
Command: []string{exe, "-mode", "logging"},
Environment: []string{
"PLUGINS_INPUTS_EXECD_MODE=application",
"MESSAGE=this is an error",
},
Signal: "STDIN",
StopOnError: true,
RestartDelay: config.Duration(100 * time.Millisecond),
Log: &l,
}
parser := models.NewRunningParser(&influx.Parser{}, &models.ParserConfig{})
require.NoError(t, parser.Init())
plugin.SetParser(parser)
// Run the plugin and trigger a report
var acc testutil.Accumulator
require.NoError(t, plugin.Start(&acc))
defer plugin.Stop()
require.NoError(t, plugin.Gather(&acc))
plugin.Stop()
// Wait for at least two metric as this indicates the process was restarted
require.Eventually(t, func() bool {
return acc.NMetrics() > 0 && l.NMessages() > 0
}, 3*time.Second, 100*time.Millisecond)
// Check the metric
expected := []telegraf.Metric{
metric.New("test", map[string]string{}, map[string]interface{}{"value": int64(0)}, time.Unix(0, 0)),
}
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
// Check the error message type
expectedLevel := byte(testutil.LevelError)
levels := make(map[byte]int, 0)
for _, m := range l.Messages() {
if strings.HasPrefix(m.Text, "Starting process") || strings.HasSuffix(m.Text, "shut down") {
continue
}
if m.Level != expectedLevel {
t.Logf("received msg %q (%s)", m.Text, string(m.Level))
} else {
require.Equal(t, "stderr: \"this is an error\"", m.Text)
}
levels[m.Level]++
}
require.Equal(t, 1, levels[testutil.LevelError])
require.Len(t, levels, 1)
}
func TestLoggingWithPrefix(t *testing.T) {
// Use own test as mocking executable
exe, err := os.Executable()
require.NoError(t, err)
tests := []struct {
name string
level byte
}{
{"error", testutil.LevelError},
{"warn", testutil.LevelWarn},
{"info", testutil.LevelInfo},
{"debug", testutil.LevelDebug},
{"trace", testutil.LevelTrace},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup the plugin with a capturing logger
var l testutil.CaptureLogger
plugin := &Execd{
Command: []string{exe, "-mode", "logging"},
Environment: []string{
"PLUGINS_INPUTS_EXECD_MODE=application",
fmt.Sprintf("MESSAGE=%s! a log message", string(tt.level)),
},
Signal: "STDIN",
StopOnError: true,
RestartDelay: config.Duration(100 * time.Millisecond),
Log: &l,
}
parser := models.NewRunningParser(&influx.Parser{}, &models.ParserConfig{})
require.NoError(t, parser.Init())
plugin.SetParser(parser)
// Run the plugin and trigger a report
var acc testutil.Accumulator
require.NoError(t, plugin.Start(&acc))
defer plugin.Stop()
require.NoError(t, plugin.Gather(&acc))
plugin.Stop()
// Wait for at least two metric as this indicates the process was restarted
require.Eventually(t, func() bool {
return acc.NMetrics() > 0 && l.NMessages() > 0
}, 3*time.Second, 100*time.Millisecond)
// Check the metric
expected := []telegraf.Metric{
metric.New("test", map[string]string{}, map[string]interface{}{"value": int64(0)}, time.Unix(0, 0)),
}
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
// Check the error message type
expectedLevel := tt.level
levels := make(map[byte]int, 0)
for _, m := range l.Messages() {
if strings.HasPrefix(m.Text, "Starting process") || strings.HasSuffix(m.Text, "shut down") {
continue
}
if m.Level != expectedLevel {
t.Logf("received msg %q (%s)", m.Text, string(m.Level))
} else {
require.Equal(t, "a log message", m.Text)
}
levels[m.Level]++
}
require.Equal(t, 1, levels[tt.level])
require.Len(t, levels, 1)
})
}
}
func readChanWithTimeout(t *testing.T, metrics chan telegraf.Metric, timeout time.Duration) telegraf.Metric {
to := time.NewTimer(timeout)
defer to.Stop()
select {
case m := <-metrics:
return m
case <-to.C:
require.Fail(t, "Timeout waiting for metric")
}
return nil
}
type TestMetricMaker struct{}
func (*TestMetricMaker) Name() string {
return "TestPlugin"
}
func (tm *TestMetricMaker) LogName() string {
return tm.Name()
}
func (*TestMetricMaker) MakeMetric(aMetric telegraf.Metric) telegraf.Metric {
return aMetric
}
func (*TestMetricMaker) Log() telegraf.Logger {
return logger.New("TestPlugin", "test", "")
}
func TestMain(m *testing.M) {
var mode string
flag.StringVar(&mode, "mode", "counter", "determines the output when run as mockup program")
flag.Parse()
operationMode := os.Getenv("PLUGINS_INPUTS_EXECD_MODE")
if operationMode != "application" {
// Run the normal test mode
os.Exit(m.Run())
}
// Run as a mock program
switch mode {
case "counter":
if err := runCounterProgram(); err != nil {
os.Exit(1)
}
os.Exit(0)
case "fail":
os.Exit(42)
case "success":
fmt.Println("test value=42i")
os.Exit(0)
case "logging":
if err := runLoggingProgram(); err != nil {
os.Exit(1)
}
os.Exit(0)
}
os.Exit(23)
}
func runCounterProgram() error {
envMetricName := os.Getenv("METRIC_NAME")
serializer := &serializers_influx.Serializer{}
if err := serializer.Init(); err != nil {
return err
}
i := 0
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
m := metric.New(envMetricName,
map[string]string{},
map[string]interface{}{"count": i},
time.Now(),
)
i++
b, err := serializer.Serialize(m)
if err != nil {
fmt.Fprintf(os.Stderr, "ERR %v\n", err)
return err
}
if _, err := fmt.Fprint(os.Stdout, string(b)); err != nil {
return err
}
}
return nil
}
func runLoggingProgram() error {
msg := os.Getenv("MESSAGE")
i := 0
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
if _, err := fmt.Fprintf(os.Stdout, "test value=%di\n", i); err != nil {
return err
}
if msg != "" {
if _, err := fmt.Fprintln(os.Stderr, msg); err != nil {
return err
}
}
}
return nil
}

View file

@ -0,0 +1,38 @@
//go:build windows
package execd
import (
"errors"
"fmt"
"io"
"os"
"time"
"github.com/influxdata/telegraf"
)
func (e *Execd) Gather(_ telegraf.Accumulator) error {
if e.process == nil {
return nil
}
switch e.Signal {
case "STDIN":
if osStdin, ok := e.process.Stdin.(*os.File); ok {
if err := osStdin.SetWriteDeadline(time.Now().Add(1 * time.Second)); err != nil {
if !errors.Is(err, os.ErrNoDeadline) {
return fmt.Errorf("setting write deadline failed: %w", err)
}
}
}
if _, err := io.WriteString(e.process.Stdin, "\n"); err != nil {
return fmt.Errorf("error writing to stdin: %w", err)
}
case "none":
default:
return fmt.Errorf("invalid signal: %s", e.Signal)
}
return nil
}

View file

@ -0,0 +1,38 @@
# Run executable as long-running input plugin
[[inputs.execd]]
## One program to run as daemon.
## NOTE: process and each argument should each be their own string
command = ["telegraf-smartctl", "-d", "/dev/sda"]
## Environment variables
## Array of "key=value" pairs to pass as environment variables
## e.g. "KEY=value", "USERNAME=John Doe",
## "LD_LIBRARY_PATH=/opt/custom/lib64:/usr/local/libs"
# environment = []
## Define how the process is signaled on each collection interval.
## Valid values are:
## "none" : Do not signal anything. (Recommended for service inputs)
## The process must output metrics by itself.
## "STDIN" : Send a newline on STDIN. (Recommended for gather inputs)
## "SIGHUP" : Send a HUP signal. Not available on Windows. (not recommended)
## "SIGUSR1" : Send a USR1 signal. Not available on Windows.
## "SIGUSR2" : Send a USR2 signal. Not available on Windows.
# signal = "none"
## Delay before the process is restarted after an unexpected termination
# restart_delay = "10s"
## Buffer size used to read from the command output stream
## Optional parameter. Default is 64 Kib, minimum is 16 bytes
# buffer_size = "64Kib"
## Disable automatic restart of the program and stop if the program exits
## with an error (i.e. non-zero error code)
# stop_on_error = false
## Data format to consume.
## 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_INPUT.md
# data_format = "influx"

View file

@ -0,0 +1,3 @@
# Telegraf Execd Go Shim
This is deprecated. Please see [/plugins/common/shim/README.md](https://github.com/influxdata/telegraf/tree/master/plugins/common/shim/README.md)

View file

@ -0,0 +1,3 @@
package main
// see /plugins/common/shim/example/cmd/main.go instead.

View file

@ -0,0 +1,2 @@
[[inputs.my_plugin_name]]
value_name = "value"

View file

@ -0,0 +1,330 @@
package shim
// this package is deprecated. use plugins/common/shim instead
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"github.com/BurntSushi/toml"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/agent"
"github.com/influxdata/telegraf/plugins/inputs"
"github.com/influxdata/telegraf/plugins/serializers/influx"
)
var (
envVarEscaper = strings.NewReplacer(
`"`, `\"`,
`\`, `\\`,
)
oldpkg = "github.com/influxdata/telegraf/plugins/inputs/execd/shim"
newpkg = "github.com/influxdata/telegraf/plugins/common/shim"
)
const (
// PollIntervalDisabled is used to indicate that you want to disable polling,
// as opposed to duration 0 meaning poll constantly.
PollIntervalDisabled = time.Duration(0)
)
// Shim allows you to wrap your inputs and run them as if they were part of Telegraf,
// except built externally.
type Shim struct {
Inputs []telegraf.Input
gatherPromptChans []chan empty
metricCh chan telegraf.Metric
stdin io.Reader
stdout io.Writer
stderr io.Writer
}
type empty struct{}
// New creates a new shim interface
func New() *Shim {
fmt.Fprintf(os.Stderr, "%s is deprecated; please change your import to %s\n", oldpkg, newpkg)
return &Shim{
stdin: os.Stdin,
stdout: os.Stdout,
stderr: os.Stderr,
}
}
// AddInput adds the input to the shim. Later calls to Run() will run this input.
func (s *Shim) AddInput(input telegraf.Input) error {
if p, ok := input.(telegraf.Initializer); ok {
err := p.Init()
if err != nil {
return fmt.Errorf("failed to init input: %w", err)
}
}
s.Inputs = append(s.Inputs, input)
return nil
}
// AddInputs adds multiple inputs to the shim. Later calls to Run() will run these.
func (s *Shim) AddInputs(newInputs []telegraf.Input) error {
for _, inp := range newInputs {
if err := s.AddInput(inp); err != nil {
return err
}
}
return nil
}
// Run the input plugins..
func (s *Shim) Run(pollInterval time.Duration) error {
// context is used only to close the stdin reader. everything else cascades
// from that point and closes cleanly when it's done.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
s.metricCh = make(chan telegraf.Metric, 1)
wg := sync.WaitGroup{}
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
collectMetricsPrompt := make(chan os.Signal, 1)
listenForCollectMetricsSignals(ctx, collectMetricsPrompt)
serializer := &influx.Serializer{}
if err := serializer.Init(); err != nil {
return fmt.Errorf("creating serializer failed: %w", err)
}
for _, input := range s.Inputs {
wrappedInput := inputShim{Input: input}
acc := agent.NewAccumulator(wrappedInput, s.metricCh)
acc.SetPrecision(time.Nanosecond)
if serviceInput, ok := input.(telegraf.ServiceInput); ok {
if err := serviceInput.Start(acc); err != nil {
return fmt.Errorf("failed to start input: %w", err)
}
}
gatherPromptCh := make(chan empty, 1)
s.gatherPromptChans = append(s.gatherPromptChans, gatherPromptCh)
wg.Add(1) // one per input
go func(input telegraf.Input) {
s.startGathering(ctx, input, acc, gatherPromptCh, pollInterval)
if serviceInput, ok := input.(telegraf.ServiceInput); ok {
serviceInput.Stop()
}
close(gatherPromptCh)
wg.Done()
}(input)
}
go s.stdinCollectMetricsPrompt(ctx, cancel, collectMetricsPrompt)
go s.closeMetricChannelWhenInputsFinish(&wg)
loop:
for {
select {
case <-quit: // user-triggered quit
// cancel, but keep looping until the metric channel closes.
cancel()
case _, open := <-collectMetricsPrompt:
if !open { // stdin-close-triggered quit
cancel()
continue
}
s.collectMetrics(ctx)
case m, open := <-s.metricCh:
if !open {
break loop
}
b, err := serializer.Serialize(m)
if err != nil {
return fmt.Errorf("failed to serialize metric: %w", err)
}
// Write this to stdout
if _, err := fmt.Fprint(s.stdout, string(b)); err != nil {
return fmt.Errorf("failed to write %q to stdout: %w", string(b), err)
}
}
}
return nil
}
// LoadConfig loads and adds the inputs to the shim
func (s *Shim) LoadConfig(filePath *string) error {
loadedInputs, err := LoadConfig(filePath)
if err != nil {
return err
}
return s.AddInputs(loadedInputs)
}
// DefaultImportedPlugins defaults to whatever plugins happen to be loaded and
// have registered themselves with the registry. This makes loading plugins
// without having to define a config dead easy.
func DefaultImportedPlugins() (i []telegraf.Input, e error) {
for _, inputCreatorFunc := range inputs.Inputs {
i = append(i, inputCreatorFunc())
}
return i, nil
}
// LoadConfig loads the config and returns inputs that later need to be loaded.
func LoadConfig(filePath *string) ([]telegraf.Input, error) {
if filePath == nil || *filePath == "" {
return DefaultImportedPlugins()
}
b, err := os.ReadFile(*filePath)
if err != nil {
return nil, err
}
s := expandEnvVars(b)
conf := struct {
Inputs map[string][]toml.Primitive
}{}
md, err := toml.Decode(s, &conf)
if err != nil {
return nil, err
}
return loadConfigIntoInputs(md, conf.Inputs)
}
func hasQuit(ctx context.Context) bool {
select {
case <-ctx.Done():
return true
default:
return false
}
}
func (s *Shim) stdinCollectMetricsPrompt(ctx context.Context, cancel context.CancelFunc, collectMetricsPrompt chan<- os.Signal) {
defer func() {
cancel()
close(collectMetricsPrompt)
}()
scanner := bufio.NewScanner(s.stdin)
// for every line read from stdin, make sure we're not supposed to quit,
// then push a message on to the collectMetricsPrompt
for scanner.Scan() {
// first check if we should quit
if hasQuit(ctx) {
return
}
// now push a non-blocking message to trigger metric collection.
pushCollectMetricsRequest(collectMetricsPrompt)
}
}
// pushCollectMetricsRequest pushes a non-blocking (nil) message to the
// collectMetricsPrompt channel to trigger metric collection.
// The channel is defined with a buffer of 1, so while it's full, subsequent
// requests are discarded.
func pushCollectMetricsRequest(collectMetricsPrompt chan<- os.Signal) {
select {
case collectMetricsPrompt <- nil:
default:
}
}
func (s *Shim) collectMetrics(ctx context.Context) {
if hasQuit(ctx) {
return
}
for i := 0; i < len(s.gatherPromptChans); i++ {
// push a message out to each channel to collect metrics. don't block.
select {
case s.gatherPromptChans[i] <- empty{}:
default:
}
}
}
func (s *Shim) startGathering(ctx context.Context, input telegraf.Input, acc telegraf.Accumulator, gatherPromptCh <-chan empty, pollInterval time.Duration) {
if pollInterval == PollIntervalDisabled {
return // don't poll
}
t := time.NewTicker(pollInterval)
defer t.Stop()
for {
// give priority to stopping.
if hasQuit(ctx) {
return
}
// see what's up
select {
case <-ctx.Done():
return
case <-gatherPromptCh:
if err := input.Gather(acc); err != nil {
if _, perr := fmt.Fprintf(s.stderr, "failed to gather metrics: %s", err); perr != nil {
acc.AddError(err)
acc.AddError(perr)
}
}
case <-t.C:
if err := input.Gather(acc); err != nil {
if _, perr := fmt.Fprintf(s.stderr, "failed to gather metrics: %s", err); perr != nil {
acc.AddError(err)
acc.AddError(perr)
}
}
}
}
}
func expandEnvVars(contents []byte) string {
return os.Expand(string(contents), getEnv)
}
func getEnv(key string) string {
v := os.Getenv(key)
return envVarEscaper.Replace(v)
}
func loadConfigIntoInputs(md toml.MetaData, inputConfigs map[string][]toml.Primitive) ([]telegraf.Input, error) {
renderedInputs := make([]telegraf.Input, 0, len(inputConfigs))
for name, primitives := range inputConfigs {
inputCreator, ok := inputs.Inputs[name]
if !ok {
return nil, errors.New("unknown input " + name)
}
for _, primitive := range primitives {
inp := inputCreator()
// Parse specific configuration
if err := md.PrimitiveDecode(primitive, inp); err != nil {
return nil, err
}
renderedInputs = append(renderedInputs, inp)
}
}
return renderedInputs, nil
}
func (s *Shim) closeMetricChannelWhenInputsFinish(wg *sync.WaitGroup) {
wg.Wait()
close(s.metricCh)
}

View file

@ -0,0 +1,20 @@
//go:build !windows
package shim
import (
"context"
"os"
"os/signal"
"syscall"
)
func listenForCollectMetricsSignals(ctx context.Context, collectMetricsPrompt chan os.Signal) {
// just listen to all the signals.
signal.Notify(collectMetricsPrompt, syscall.SIGHUP, syscall.SIGUSR1, syscall.SIGUSR2)
go func() {
<-ctx.Done()
signal.Stop(collectMetricsPrompt)
}()
}

View file

@ -0,0 +1,20 @@
//go:build windows
package shim
import (
"context"
"os"
"os/signal"
"syscall"
)
func listenForCollectMetricsSignals(ctx context.Context, collectMetricsPrompt chan os.Signal) {
signal.Notify(collectMetricsPrompt, syscall.SIGHUP)
go func() {
<-ctx.Done()
// context done. stop to signals to avoid pushing messages to a closed channel
signal.Stop(collectMetricsPrompt)
}()
}

View file

@ -0,0 +1,23 @@
package shim
import "github.com/influxdata/telegraf"
// inputShim implements the MetricMaker interface.
type inputShim struct {
Input telegraf.Input
}
// LogName satisfies the MetricMaker interface
func (inputShim) LogName() string {
return ""
}
// MakeMetric satisfies the MetricMaker interface
func (inputShim) MakeMetric(m telegraf.Metric) telegraf.Metric {
return m // don't need to do anything to it.
}
// Log satisfies the MetricMaker interface
func (inputShim) Log() telegraf.Logger {
return nil
}

View file

@ -0,0 +1,62 @@
//go:build !windows
package shim
import (
"bufio"
"context"
"io"
"os"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestShimUSR1SignalingWorks(t *testing.T) {
stdinReader, stdinWriter := io.Pipe()
stdoutReader, stdoutWriter := io.Pipe()
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
metricProcessed, exited := runInputPlugin(t, 20*time.Minute, stdinReader, stdoutWriter, nil)
// signal USR1 to yourself.
pid := os.Getpid()
process, err := os.FindProcess(pid)
require.NoError(t, err)
go func() {
// On slow machines this signal can fire before the service comes up.
// rather than depend on accurate sleep times, we'll just retry sending
// the signal every so often until it goes through.
for {
select {
case <-ctx.Done():
return // test is done
default:
// test isn't done, keep going.
if err := process.Signal(syscall.SIGUSR1); err != nil {
t.Error(err)
metricProcessed <- false
return
}
time.Sleep(200 * time.Millisecond)
}
}
}()
<-metricProcessed
cancel()
r := bufio.NewReader(stdoutReader)
out, err := r.ReadString('\n')
require.NoError(t, err)
require.Equal(t, "measurement,tag=tag field=1i 1234000005678\n", out)
require.NoError(t, stdinWriter.Close())
readUntilEmpty(r)
<-exited
}

View file

@ -0,0 +1,169 @@
package shim
import (
"bufio"
"io"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/inputs"
)
func TestShimWorks(t *testing.T) {
stdoutReader, stdoutWriter := io.Pipe()
stdin, _ := io.Pipe() // hold the stdin pipe open
metricProcessed, _ := runInputPlugin(t, 10*time.Millisecond, stdin, stdoutWriter, nil)
<-metricProcessed
r := bufio.NewReader(stdoutReader)
out, err := r.ReadString('\n')
require.NoError(t, err)
require.Contains(t, out, "\n")
metricLine := strings.Split(out, "\n")[0]
require.Equal(t, "measurement,tag=tag field=1i 1234000005678", metricLine)
}
func TestShimStdinSignalingWorks(t *testing.T) {
stdinReader, stdinWriter := io.Pipe()
stdoutReader, stdoutWriter := io.Pipe()
metricProcessed, exited := runInputPlugin(t, 40*time.Second, stdinReader, stdoutWriter, nil)
_, err := stdinWriter.Write([]byte("\n"))
require.NoError(t, err)
<-metricProcessed
r := bufio.NewReader(stdoutReader)
out, err := r.ReadString('\n')
require.NoError(t, err)
require.Equal(t, "measurement,tag=tag field=1i 1234000005678\n", out)
require.NoError(t, stdinWriter.Close())
readUntilEmpty(r)
// check that it exits cleanly
<-exited
}
func runInputPlugin(t *testing.T, interval time.Duration, stdin io.Reader, stdout, stderr io.Writer) (processed, exited chan bool) {
processed = make(chan bool)
exited = make(chan bool)
inp := &testInput{
metricProcessed: processed,
}
shim := New()
if stdin != nil {
shim.stdin = stdin
}
if stdout != nil {
shim.stdout = stdout
}
if stderr != nil {
shim.stderr = stderr
}
require.NoError(t, shim.AddInput(inp))
go func(e chan bool) {
if err := shim.Run(interval); err != nil {
t.Error(err)
}
e <- true
}(exited)
return processed, exited
}
type testInput struct {
metricProcessed chan bool
}
func (*testInput) SampleConfig() string {
return ""
}
func (i *testInput) Gather(acc telegraf.Accumulator) error {
acc.AddFields("measurement",
map[string]interface{}{
"field": 1,
},
map[string]string{
"tag": "tag",
}, time.Unix(1234, 5678))
i.metricProcessed <- true
return nil
}
func (*testInput) Start(telegraf.Accumulator) error {
return nil
}
func (*testInput) Stop() {
}
func TestLoadConfig(t *testing.T) {
t.Setenv("SECRET_TOKEN", "xxxxxxxxxx")
t.Setenv("SECRET_VALUE", `test"\test`)
inputs.Add("test", func() telegraf.Input {
return &serviceInput{}
})
c := "./testdata/plugin.conf"
loadedInputs, err := LoadConfig(&c)
require.NoError(t, err)
inp := loadedInputs[0].(*serviceInput)
require.Equal(t, "awesome name", inp.ServiceName)
require.Equal(t, "xxxxxxxxxx", inp.SecretToken)
require.Equal(t, `test"\test`, inp.SecretValue)
}
type serviceInput struct {
ServiceName string `toml:"service_name"`
SecretToken string `toml:"secret_token"`
SecretValue string `toml:"secret_value"`
}
func (*serviceInput) SampleConfig() string {
return ""
}
func (*serviceInput) Gather(acc telegraf.Accumulator) error {
acc.AddFields("measurement",
map[string]interface{}{
"field": 1,
},
map[string]string{
"tag": "tag",
}, time.Unix(1234, 5678))
return nil
}
func (*serviceInput) Start(telegraf.Accumulator) error {
return nil
}
func (*serviceInput) Stop() {
}
// we can get stuck if stdout gets clogged up and nobody's reading from it.
// make sure we keep it going
func readUntilEmpty(r *bufio.Reader) {
go func() {
var err error
for err != io.EOF {
_, err = r.ReadString('\n')
time.Sleep(10 * time.Millisecond)
}
}()
}

View file

@ -0,0 +1,4 @@
[[inputs.test]]
service_name = "awesome name"
secret_token = "${SECRET_TOKEN}"
secret_value = "$SECRET_VALUE"