180 lines
4.1 KiB
Go
180 lines
4.1 KiB
Go
|
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{}
|
||
|
})
|
||
|
}
|