Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
100
plugins/inputs/execd/README.md
Normal file
100
plugins/inputs/execd/README.md
Normal 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.
|
24
plugins/inputs/execd/examples/count.go
Normal file
24
plugins/inputs/execd/examples/count.go
Normal 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++
|
||||
}
|
||||
}
|
12
plugins/inputs/execd/examples/count.py
Normal file
12
plugins/inputs/execd/examples/count.py
Normal 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)
|
21
plugins/inputs/execd/examples/count.rb
Normal file
21
plugins/inputs/execd/examples/count.rb
Normal 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
|
12
plugins/inputs/execd/examples/count.sh
Normal file
12
plugins/inputs/execd/examples/count.sh
Normal 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
|
186
plugins/inputs/execd/execd.go
Normal file
186
plugins/inputs/execd/execd.go
Normal 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),
|
||||
}
|
||||
})
|
||||
}
|
46
plugins/inputs/execd/execd_posix.go
Normal file
46
plugins/inputs/execd/execd_posix.go
Normal 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
|
||||
}
|
459
plugins/inputs/execd/execd_test.go
Normal file
459
plugins/inputs/execd/execd_test.go
Normal 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
|
||||
}
|
38
plugins/inputs/execd/execd_windows.go
Normal file
38
plugins/inputs/execd/execd_windows.go
Normal 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
|
||||
}
|
38
plugins/inputs/execd/sample.conf
Normal file
38
plugins/inputs/execd/sample.conf
Normal 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"
|
3
plugins/inputs/execd/shim/README.md
Normal file
3
plugins/inputs/execd/shim/README.md
Normal 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)
|
3
plugins/inputs/execd/shim/example/cmd/main.go
Normal file
3
plugins/inputs/execd/shim/example/cmd/main.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package main
|
||||
|
||||
// see /plugins/common/shim/example/cmd/main.go instead.
|
2
plugins/inputs/execd/shim/example/cmd/plugin.conf
Normal file
2
plugins/inputs/execd/shim/example/cmd/plugin.conf
Normal file
|
@ -0,0 +1,2 @@
|
|||
[[inputs.my_plugin_name]]
|
||||
value_name = "value"
|
330
plugins/inputs/execd/shim/goshim.go
Normal file
330
plugins/inputs/execd/shim/goshim.go
Normal 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)
|
||||
}
|
20
plugins/inputs/execd/shim/goshim_posix.go
Normal file
20
plugins/inputs/execd/shim/goshim_posix.go
Normal 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)
|
||||
}()
|
||||
}
|
20
plugins/inputs/execd/shim/goshim_windows.go
Normal file
20
plugins/inputs/execd/shim/goshim_windows.go
Normal 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)
|
||||
}()
|
||||
}
|
23
plugins/inputs/execd/shim/input.go
Normal file
23
plugins/inputs/execd/shim/input.go
Normal 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
|
||||
}
|
62
plugins/inputs/execd/shim/shim_posix_test.go
Normal file
62
plugins/inputs/execd/shim/shim_posix_test.go
Normal 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
|
||||
}
|
169
plugins/inputs/execd/shim/shim_test.go
Normal file
169
plugins/inputs/execd/shim/shim_test.go
Normal 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)
|
||||
}
|
||||
}()
|
||||
}
|
4
plugins/inputs/execd/shim/testdata/plugin.conf
vendored
Normal file
4
plugins/inputs/execd/shim/testdata/plugin.conf
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
[[inputs.test]]
|
||||
service_name = "awesome name"
|
||||
secret_token = "${SECRET_TOKEN}"
|
||||
secret_value = "$SECRET_VALUE"
|
Loading…
Add table
Add a link
Reference in a new issue