telegraf/plugins/inputs/execline/execline.go

180 lines
4.1 KiB
Go
Raw Normal View History

package execline
import (
"bufio"
"bytes"
"fmt"
"math"
"os/exec"
"path/filepath"
"strings"
"sync"
"github.com/gonuts/go-shellquote"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/inputs"
)
const sampleConfig = `
# NOTE This execline plugin only reads numerical measurements output by commands,
# strings and booleans ill be ignored.
commands = ["/tmp/test.sh","/tmp/test2.sh"] # the bind address
### If matching multiple measurement files, this string will be used to join the matched values.
separator = "."
### Default tags that will be added to all metrics. These can be overridden at the template level
### or by tags extracted from metric
tags = ["region=north-china", "zone=1c"]
### Each template line requires a template pattern. It can have an optional
### filter before the template and separated by spaces. It can also have optional extra
### tags following the template. Multiple tags should be separated by commas and no spaces
### similar to the line protocol format. The can be only one default template.
### Templates support below format:
### filter + template
### filter + template + extra tag
### filter + template with field key
### default template. Ignore the first graphite component "servers"
templates = [
"*.app env.service.resource.measurement",
"stats.* .host.measurement* region=us-west,agent=sensu",
"stats2.* .host.measurement.field",
"measurement*"
]
`
type ExecLine struct {
Commands []string
Separator string
Tags []string
Templates []string
parser *Parser
config *Config
initedConfig bool
wg sync.WaitGroup
sync.Mutex
}
func (e *ExecLine) Run(command string, acc telegraf.Accumulator) error {
defer e.wg.Done()
split_cmd, err := shellquote.Split(command)
if err != nil || len(split_cmd) == 0 {
return fmt.Errorf("execline: unable to parse command, %s", err)
}
cmd := exec.Command(split_cmd[0], split_cmd[1:]...)
name := strings.Replace(filepath.Base(cmd.Path), "/", "_", -1)
name = strings.Replace(name, ".", "_", -1)
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return fmt.Errorf("execline: %s for command '%s'", err, command)
}
reader := bufio.NewReader(&out)
for {
// Read up to the next newline.
buf, err := reader.ReadBytes('\n')
if err != nil {
return err
}
// Trim the buffer, even though there should be no padding
line := strings.TrimSpace(string(buf))
e.handleLine(name, acc, line)
}
return nil
}
func (e *ExecLine) handleLine(name string, acc telegraf.Accumulator, line string) {
if line == "" {
return
}
// Parse it.
metric, err := e.parser.Parse(line)
if err != nil {
switch err := err.(type) {
case *UnsupposedValueError:
// Graphite ignores NaN values with no error.
if math.IsNaN(err.Value) {
return
}
}
fmt.Errorf("unable to parse line: %s: %s", line, err)
return
}
acc.AddFields(name+"."+metric.Name(), metric.Fields(), metric.Tags(), metric.Time())
}
func (e *ExecLine) initConfig() error {
e.Lock()
defer e.Unlock()
c := &Config{
Commands: e.Commands,
Separator: e.Separator,
Tags: e.Tags,
Templates: e.Templates,
}
c.WithDefaults()
if err := c.Validate(); err != nil {
return fmt.Errorf("ExecLine configuration is error! ", err.Error())
}
e.config = c
parser, err := NewParserWithOptions(Options{
Templates: e.config.Templates,
DefaultTags: e.config.DefaultTags(),
Separator: e.config.Separator})
if err != nil {
return fmt.Errorf("ExecLine input parser config is error! ", err.Error())
}
e.parser = parser
return nil
}
func (e *ExecLine) SampleConfig() string {
return sampleConfig
}
func (e *ExecLine) Description() string {
return "Read metrics from one or more commands that output graphite line protocol to stdout"
}
func (e *ExecLine) Gather(acc telegraf.Accumulator) error {
if !e.initedConfig {
if err := e.initConfig(); err != nil {
return err
}
e.initedConfig = true
}
for _, command := range e.Commands {
e.wg.Add(1)
go e.Run(command, acc)
}
e.wg.Wait()
return nil
}
func init() {
inputs.Add("execline", func() telegraf.Input {
return &ExecLine{}
})
}