Add native Go ping method to ping input plugin (#6050)

This commit is contained in:
Greg 2019-07-11 16:07:58 -06:00 committed by Daniel Nelson
parent c9107015b0
commit ea6b398fa3
6 changed files with 492 additions and 321 deletions

10
Gopkg.lock generated
View File

@ -423,6 +423,14 @@
pruneopts = "" pruneopts = ""
revision = "25d852aebe32c875e9c044af3eef9c7dc6bc777f" revision = "25d852aebe32c875e9c044af3eef9c7dc6bc777f"
[[projects]]
digest = "1:c6f371f2b02c751a83be83139a12a5467e55393feda16d4f8dfa95adfc4efede"
name = "github.com/glinton/ping"
packages = ["."]
pruneopts = ""
revision = "1983bc2fd5de3ea00aa5457bbc8774300e889db9"
version = "v0.1.1"
[[projects]] [[projects]]
digest = "1:858b7fe7b0f4bc7ef9953926828f2816ea52d01a88d72d1c45bc8c108f23c356" digest = "1:858b7fe7b0f4bc7ef9953926828f2816ea52d01a88d72d1c45bc8c108f23c356"
name = "github.com/go-ini/ini" name = "github.com/go-ini/ini"
@ -1266,6 +1274,7 @@
"http/httpguts", "http/httpguts",
"http2", "http2",
"http2/hpack", "http2/hpack",
"icmp",
"idna", "idna",
"internal/iana", "internal/iana",
"internal/socket", "internal/socket",
@ -1603,6 +1612,7 @@
"github.com/ericchiang/k8s/apis/resource", "github.com/ericchiang/k8s/apis/resource",
"github.com/ericchiang/k8s/util/intstr", "github.com/ericchiang/k8s/util/intstr",
"github.com/ghodss/yaml", "github.com/ghodss/yaml",
"github.com/glinton/ping",
"github.com/go-logfmt/logfmt", "github.com/go-logfmt/logfmt",
"github.com/go-redis/redis", "github.com/go-redis/redis",
"github.com/go-sql-driver/mysql", "github.com/go-sql-driver/mysql",

View File

@ -9,6 +9,10 @@ use the iputils-ping implementation:
apt-get install iputils-ping apt-get install iputils-ping
``` ```
When using `method = "native"` a ping is sent and the results are reported in pure go, eliminating the need to execute the system `ping` command. Not using the system binary allows the use of this plugin on non-english systems.
There is currently no support for TTL on windows with `"native"`; track progress at https://github.com/golang/go/issues/7175 and https://github.com/golang/go/issues/7174
### Configuration: ### Configuration:
```toml ```toml
@ -33,12 +37,18 @@ apt-get install iputils-ping
## on Darwin and Freebsd only source address possible: (ping -S <SRC_ADDR>) ## on Darwin and Freebsd only source address possible: (ping -S <SRC_ADDR>)
# interface = "" # interface = ""
## How to ping. "native" doesn't have external dependencies, while "exec" depends on 'ping'.
# method = "exec"
## Specify the ping executable binary, default is "ping" ## Specify the ping executable binary, default is "ping"
# binary = "ping" # binary = "ping"
## Arguments for ping command ## Arguments for ping command. When arguments is not empty, system binary will be used and
## when arguments is not empty, other options (ping_interval, timeout, etc) will be ignored ## other options (ping_interval, timeout, etc) will be ignored
# arguments = ["-c", "3"] # arguments = ["-c", "3"]
## Use only ipv6 addresses when resolving hostnames.
# ipv6 = false
``` ```
#### File Limit #### File Limit
@ -62,6 +72,21 @@ Set the file number limit:
LimitNOFILE=4096 LimitNOFILE=4096
``` ```
#### Permission Caveat (non Windows)
It is preferred that this plugin listen on privileged ICMP sockets. To do so, telegraf can either be run as the root user or the root user can add the capability to access raw sockets to telegraf by running the following commant:
```
setcap cap_net_raw=eip /path/to/telegraf
```
Another option (doesn't work as well or in all circumstances) is to listen on unprivileged raw sockets (non-Windows only). The system group of the user running telegraf must be allowed to create ICMP Echo sockets. [See man pages icmp(7) for `ping_group_range`](http://man7.org/linux/man-pages/man7/icmp.7.html). On Linux hosts, run the following to give a group the proper permissions:
```
sudo sysctl -w net.ipv4.ping_group_range="GROUP_ID_LOW GROUP_ID_HIGH"
```
### Metrics: ### Metrics:
- ping - ping
@ -75,15 +100,15 @@ LimitNOFILE=4096
- average_response_ms (integer) - average_response_ms (integer)
- minimum_response_ms (integer) - minimum_response_ms (integer)
- maximum_response_ms (integer) - maximum_response_ms (integer)
- standard_deviation_ms (integer, Not available on Windows) - standard_deviation_ms (integer, Available on Windows only with native ping)
- errors (float, Windows only) - errors (float, Windows only)
- reply_received (integer, Windows only) - reply_received (integer, Windows only*)
- percent_reply_loss (float, Windows only) - percent_reply_loss (float, Windows only*)
- result_code (int, success = 0, no such host = 1, ping error = 2) - result_code (int, success = 0, no such host = 1, ping error = 2)
##### reply_received vs packets_received ##### reply_received vs packets_received
On Windows systems, "Destination net unreachable" reply will increment `packets_received` but not `reply_received`. On Windows systems, "Destination net unreachable" reply will increment `packets_received` but not `reply_received`*
### Example Output: ### Example Output:
@ -96,3 +121,5 @@ ping,url=example.org result_code=0i,average_response_ms=7i,maximum_response_ms=9
``` ```
ping,url=example.org average_response_ms=23.066,ttl=63,maximum_response_ms=24.64,minimum_response_ms=22.451,packets_received=5i,packets_transmitted=5i,percent_packet_loss=0,result_code=0i,standard_deviation_ms=0.809 1535747258000000000 ping,url=example.org average_response_ms=23.066,ttl=63,maximum_response_ms=24.64,minimum_response_ms=22.451,packets_received=5i,packets_transmitted=5i,percent_packet_loss=0,result_code=0i,standard_deviation_ms=0.809 1535747258000000000
``` ```
*not when `method = "native"` is used

View File

@ -1,20 +1,17 @@
// +build !windows
package ping package ping
import ( import (
"context"
"errors" "errors"
"fmt" "math"
"net" "net"
"os/exec" "os/exec"
"regexp"
"runtime" "runtime"
"strconv"
"strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/glinton/ping"
"github.com/influxdata/telegraf" "github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/internal" "github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/inputs" "github.com/influxdata/telegraf/plugins/inputs"
@ -34,7 +31,7 @@ type Ping struct {
// Number of pings to send (ping -c <COUNT>) // Number of pings to send (ping -c <COUNT>)
Count int Count int
// Ping timeout, in seconds. 0 means no timeout (ping -W <TIMEOUT>) // Per-ping timeout, in seconds. 0 means no timeout (ping -W <TIMEOUT>)
Timeout float64 Timeout float64
// Ping deadline, in seconds. 0 means no deadline. (ping -w <DEADLINE>) // Ping deadline, in seconds. 0 means no deadline. (ping -w <DEADLINE>)
@ -46,18 +43,27 @@ type Ping struct {
// URLs to ping // URLs to ping
Urls []string Urls []string
// Method defines how to ping (native or exec)
Method string
// Ping executable binary // Ping executable binary
Binary string Binary string
// Arguments for ping command. // Arguments for ping command. When arguments is not empty, system binary will be used and
// when `Arguments` is not empty, other options (ping_interval, timeout, etc) will be ignored // other options (ping_interval, timeout, etc) will be ignored
Arguments []string Arguments []string
// Whether to resolve addresses using ipv6 or not.
IPv6 bool
// host ping function // host ping function
pingHost HostPinger pingHost HostPinger
// listenAddr is the address associated with the interface defined.
listenAddr string
} }
func (_ *Ping) Description() string { func (*Ping) Description() string {
return "Ping given url(s) and return statistics" return "Ping given url(s) and return statistics"
} }
@ -69,7 +75,6 @@ const sampleConfig = `
# count = 1 # count = 1
## Interval, in s, at which to ping. 0 == default (ping -i <PING_INTERVAL>) ## Interval, in s, at which to ping. 0 == default (ping -i <PING_INTERVAL>)
## Not available in Windows.
# ping_interval = 1.0 # ping_interval = 1.0
## Per-ping timeout, in s. 0 == no timeout (ping -W <TIMEOUT>) ## Per-ping timeout, in s. 0 == no timeout (ping -W <TIMEOUT>)
@ -78,27 +83,53 @@ const sampleConfig = `
## Total-ping deadline, in s. 0 == no deadline (ping -w <DEADLINE>) ## Total-ping deadline, in s. 0 == no deadline (ping -w <DEADLINE>)
# deadline = 10 # deadline = 10
## Interface or source address to send ping from (ping -I <INTERFACE/SRC_ADDR>) ## Interface or source address to send ping from (ping -I[-S] <INTERFACE/SRC_ADDR>)
## on Darwin and Freebsd only source address possible: (ping -S <SRC_ADDR>)
# interface = "" # interface = ""
## Specify the ping executable binary, default is "ping" ## How to ping. "native" doesn't have external dependencies, while "exec" depends on 'ping'.
# binary = "ping" # method = "exec"
## Arguments for ping command ## Specify the ping executable binary, default is "ping"
## when arguments is not empty, other options (ping_interval, timeout, etc) will be ignored # binary = "ping"
## Arguments for ping command. When arguments is not empty, system binary will be used and
## other options (ping_interval, timeout, etc) will be ignored.
# arguments = ["-c", "3"] # arguments = ["-c", "3"]
## Use only ipv6 addresses when resolving hostnames.
# ipv6 = false
` `
func (_ *Ping) SampleConfig() string { func (*Ping) SampleConfig() string {
return sampleConfig return sampleConfig
} }
func (p *Ping) Gather(acc telegraf.Accumulator) error { func (p *Ping) Gather(acc telegraf.Accumulator) error {
// Spin off a go routine for each url to ping if p.Interface != "" && p.listenAddr != "" {
for _, url := range p.Urls { p.listenAddr = getAddr(p.Interface)
p.wg.Add(1) }
go p.pingToURL(url, acc)
for _, ip := range p.Urls {
_, err := net.LookupHost(ip)
if err != nil {
acc.AddFields("ping", map[string]interface{}{"result_code": 1}, map[string]string{"ip": ip})
acc.AddError(err)
return nil
}
if p.Method == "native" {
p.wg.Add(1)
go func(ip string) {
defer p.wg.Done()
p.pingToURLNative(ip, acc)
}(ip)
} else {
p.wg.Add(1)
go func(ip string) {
defer p.wg.Done()
p.pingToURL(ip, acc)
}(ip)
}
} }
p.wg.Wait() p.wg.Wait()
@ -106,81 +137,39 @@ func (p *Ping) Gather(acc telegraf.Accumulator) error {
return nil return nil
} }
func (p *Ping) pingToURL(u string, acc telegraf.Accumulator) { func getAddr(iface string) string {
defer p.wg.Done() if addr := net.ParseIP(iface); addr != nil {
tags := map[string]string{"url": u} return addr.String()
fields := map[string]interface{}{"result_code": 0}
_, err := net.LookupHost(u)
if err != nil {
acc.AddError(err)
fields["result_code"] = 1
acc.AddFields("ping", fields, tags)
return
} }
args := p.args(u, runtime.GOOS) ifaces, err := net.Interfaces()
totalTimeout := 60.0 if err != nil {
if len(p.Arguments) == 0 { return ""
totalTimeout = float64(p.Count)*p.Timeout + float64(p.Count-1)*p.PingInterval
} }
out, err := p.pingHost(p.Binary, totalTimeout, args...) var ip net.IP
if err != nil { for i := range ifaces {
// Some implementations of ping return a 1 exit code on if ifaces[i].Name == iface {
// timeout, if this occurs we will not exit and try to parse addrs, err := ifaces[i].Addrs()
// the output. if err != nil {
status := -1 return ""
if exitError, ok := err.(*exec.ExitError); ok { }
if ws, ok := exitError.Sys().(syscall.WaitStatus); ok { if len(addrs) > 0 {
status = ws.ExitStatus() switch v := addrs[0].(type) {
fields["result_code"] = status case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if len(ip) == 0 {
return ""
}
return ip.String()
} }
} }
if status != 1 {
// Combine go err + stderr output
out = strings.TrimSpace(out)
if len(out) > 0 {
acc.AddError(fmt.Errorf("host %s: %s, %s", u, out, err))
} else {
acc.AddError(fmt.Errorf("host %s: %s", u, err))
}
fields["result_code"] = 2
acc.AddFields("ping", fields, tags)
return
}
} }
trans, rec, ttl, min, avg, max, stddev, err := processPingOutput(out) return ""
if err != nil {
// fatal error
acc.AddError(fmt.Errorf("%s: %s", err, u))
fields["result_code"] = 2
acc.AddFields("ping", fields, tags)
return
}
// Calculate packet loss percentage
loss := float64(trans-rec) / float64(trans) * 100.0
fields["packets_transmitted"] = trans
fields["packets_received"] = rec
fields["percent_packet_loss"] = loss
if ttl >= 0 {
fields["ttl"] = ttl
}
if min >= 0 {
fields["minimum_response_ms"] = min
}
if avg >= 0 {
fields["average_response_ms"] = avg
}
if max >= 0 {
fields["maximum_response_ms"] = max
}
if stddev >= 0 {
fields["standard_deviation_ms"] = stddev
}
acc.AddFields("ping", fields, tags)
} }
func hostPinger(binary string, timeout float64, args ...string) (string, error) { func hostPinger(binary string, timeout float64, args ...string) (string, error) {
@ -194,137 +183,156 @@ func hostPinger(binary string, timeout float64, args ...string) (string, error)
return string(out), err return string(out), err
} }
// args returns the arguments for the 'ping' executable func (p *Ping) pingToURLNative(destination string, acc telegraf.Accumulator) {
func (p *Ping) args(url string, system string) []string { ctx := context.Background()
if len(p.Arguments) > 0 {
return append(p.Arguments, url) network := "ip4"
if p.IPv6 {
network = "ip6"
} }
// build the ping command args based on toml config host, err := net.ResolveIPAddr(network, destination)
args := []string{"-c", strconv.Itoa(p.Count), "-n", "-s", "16"} if err != nil {
if p.PingInterval > 0 { acc.AddFields("ping", map[string]interface{}{"result_code": 1}, map[string]string{"url": destination})
args = append(args, "-i", strconv.FormatFloat(p.PingInterval, 'f', -1, 64)) acc.AddError(err)
return
} }
if p.Timeout > 0 {
switch system { interval := p.PingInterval
case "darwin": if interval < 0.2 {
args = append(args, "-W", strconv.FormatFloat(p.Timeout*1000, 'f', -1, 64)) interval = 0.2
case "freebsd", "netbsd", "openbsd":
args = append(args, "-W", strconv.FormatFloat(p.Timeout*1000, 'f', -1, 64))
case "linux":
args = append(args, "-W", strconv.FormatFloat(p.Timeout, 'f', -1, 64))
default:
// Not sure the best option here, just assume GNU ping?
args = append(args, "-W", strconv.FormatFloat(p.Timeout, 'f', -1, 64))
}
} }
timeout := p.Timeout
if timeout == 0 {
timeout = 5
}
tick := time.NewTicker(time.Duration(interval * float64(time.Second)))
defer tick.Stop()
if p.Deadline > 0 { if p.Deadline > 0 {
switch system { var cancel context.CancelFunc
case "darwin", "freebsd", "netbsd", "openbsd": ctx, cancel = context.WithTimeout(ctx, time.Duration(p.Deadline)*time.Second)
args = append(args, "-t", strconv.Itoa(p.Deadline)) defer cancel()
case "linux": }
args = append(args, "-w", strconv.Itoa(p.Deadline))
default: resps := make(chan *ping.Response)
// not sure the best option here, just assume gnu ping? rsps := []*ping.Response{}
args = append(args, "-w", strconv.Itoa(p.Deadline))
r := &sync.WaitGroup{}
r.Add(1)
go func() {
for res := range resps {
rsps = append(rsps, res)
}
r.Done()
}()
wg := &sync.WaitGroup{}
c := ping.Client{}
var i int
for i = 0; i < p.Count; i++ {
select {
case <-ctx.Done():
goto finish
case <-tick.C:
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout*float64(time.Second)))
defer cancel()
wg.Add(1)
go func(seq int) {
defer wg.Done()
resp, err := c.Do(ctx, &ping.Request{
Dst: net.ParseIP(host.String()),
Src: net.ParseIP(p.listenAddr),
Seq: seq,
})
if err != nil {
acc.AddFields("ping", map[string]interface{}{"result_code": 2}, map[string]string{"url": destination})
acc.AddError(err)
return
}
resps <- resp
}(i + 1)
} }
} }
if p.Interface != "" {
switch system { finish:
case "darwin": wg.Wait()
args = append(args, "-I", p.Interface) close(resps)
case "freebsd", "netbsd", "openbsd":
args = append(args, "-S", p.Interface) r.Wait()
case "linux": tags, fields := onFin(i, rsps, destination)
args = append(args, "-I", p.Interface) acc.AddFields("ping", fields, tags)
default: }
// not sure the best option here, just assume gnu ping?
args = append(args, "-i", p.Interface) func onFin(packetsSent int, resps []*ping.Response, destination string) (map[string]string, map[string]interface{}) {
packetsRcvd := len(resps)
tags := map[string]string{"url": destination}
fields := map[string]interface{}{
"result_code": 0,
"packets_transmitted": packetsSent,
"packets_received": packetsRcvd,
}
if packetsSent == 0 {
return tags, fields
}
if packetsRcvd == 0 {
fields["percent_packet_loss"] = float64(100)
return tags, fields
}
fields["percent_packet_loss"] = float64(packetsSent-packetsRcvd) / float64(packetsSent) * 100
ttl := resps[0].TTL
var min, max, avg, total time.Duration
min = resps[0].RTT
max = resps[0].RTT
for _, res := range resps {
if res.RTT < min {
min = res.RTT
} }
} if res.RTT > max {
args = append(args, url) max = res.RTT
return args
}
// processPingOutput takes in a string output from the ping command, like:
//
// ping www.google.com (173.194.115.84): 56 data bytes
// 64 bytes from 173.194.115.84: icmp_seq=0 ttl=54 time=52.172 ms
// 64 bytes from 173.194.115.84: icmp_seq=1 ttl=54 time=34.843 ms
//
// --- www.google.com ping statistics ---
// 2 packets transmitted, 2 packets received, 0.0% packet loss
// round-trip min/avg/max/stddev = 34.843/43.508/52.172/8.664 ms
//
// It returns (<transmitted packets>, <received packets>, <average response>)
func processPingOutput(out string) (int, int, int, float64, float64, float64, float64, error) {
var trans, recv, ttl int = 0, 0, -1
var min, avg, max, stddev float64 = -1.0, -1.0, -1.0, -1.0
// Set this error to nil if we find a 'transmitted' line
err := errors.New("Fatal error processing ping output")
lines := strings.Split(out, "\n")
for _, line := range lines {
// Reading only first TTL, ignoring other TTL messages
if ttl == -1 && strings.Contains(line, "ttl=") {
ttl, err = getTTL(line)
} else if strings.Contains(line, "transmitted") &&
strings.Contains(line, "received") {
trans, recv, err = getPacketStats(line, trans, recv)
if err != nil {
return trans, recv, ttl, min, avg, max, stddev, err
}
} else if strings.Contains(line, "min/avg/max") {
min, avg, max, stddev, err = checkRoundTripTimeStats(line, min, avg, max, stddev)
if err != nil {
return trans, recv, ttl, min, avg, max, stddev, err
}
} }
total += res.RTT
} }
return trans, recv, ttl, min, avg, max, stddev, err
avg = total / time.Duration(packetsRcvd)
var sumsquares time.Duration
for _, res := range resps {
sumsquares += (res.RTT - avg) * (res.RTT - avg)
}
stdDev := time.Duration(math.Sqrt(float64(sumsquares / time.Duration(packetsRcvd))))
// Set TTL only on supported platform. See golang.org/x/net/ipv4/payload_cmsg.go
switch runtime.GOOS {
case "aix", "darwin", "dragonfly", "freebsd", "linux", "netbsd", "openbsd", "solaris":
fields["ttl"] = ttl
}
fields["minimum_response_ms"] = float64(min.Nanoseconds()) / float64(time.Millisecond)
fields["average_response_ms"] = float64(avg.Nanoseconds()) / float64(time.Millisecond)
fields["maximum_response_ms"] = float64(max.Nanoseconds()) / float64(time.Millisecond)
fields["standard_deviation_ms"] = float64(stdDev.Nanoseconds()) / float64(time.Millisecond)
return tags, fields
} }
func getPacketStats(line string, trans, recv int) (int, int, error) { // Init ensures the plugin is configured correctly.
stats := strings.Split(line, ", ") func (p *Ping) Init() error {
// Transmitted packets if p.Count < 1 {
trans, err := strconv.Atoi(strings.Split(stats[0], " ")[0]) return errors.New("bad number of packets to transmit")
if err != nil {
return trans, recv, err
} }
// Received packets
recv, err = strconv.Atoi(strings.Split(stats[1], " ")[0])
return trans, recv, err
}
func getTTL(line string) (int, error) { return nil
ttlLine := regexp.MustCompile(`ttl=(\d+)`)
ttlMatch := ttlLine.FindStringSubmatch(line)
return strconv.Atoi(ttlMatch[1])
}
func checkRoundTripTimeStats(line string, min, avg, max,
stddev float64) (float64, float64, float64, float64, error) {
stats := strings.Split(line, " ")[3]
data := strings.Split(stats, "/")
min, err := strconv.ParseFloat(data[0], 64)
if err != nil {
return min, avg, max, stddev, err
}
avg, err = strconv.ParseFloat(data[1], 64)
if err != nil {
return min, avg, max, stddev, err
}
max, err = strconv.ParseFloat(data[2], 64)
if err != nil {
return min, avg, max, stddev, err
}
if len(data) == 4 {
stddev, err = strconv.ParseFloat(data[3], 64)
if err != nil {
return min, avg, max, stddev, err
}
}
return min, avg, max, stddev, err
} }
func init() { func init() {
@ -335,6 +343,7 @@ func init() {
Count: 1, Count: 1,
Timeout: 1.0, Timeout: 1.0,
Deadline: 10, Deadline: 10,
Method: "exec",
Binary: "ping", Binary: "ping",
Arguments: []string{}, Arguments: []string{},
} }

View File

@ -0,0 +1,212 @@
// +build !windows
package ping
import (
"errors"
"fmt"
"os/exec"
"regexp"
"runtime"
"strconv"
"strings"
"syscall"
"github.com/influxdata/telegraf"
)
func (p *Ping) pingToURL(u string, acc telegraf.Accumulator) {
tags := map[string]string{"url": u}
fields := map[string]interface{}{"result_code": 0}
out, err := p.pingHost(p.Binary, 60.0, p.args(u, runtime.GOOS)...)
if err != nil {
// Some implementations of ping return a 1 exit code on
// timeout, if this occurs we will not exit and try to parse
// the output.
status := -1
if exitError, ok := err.(*exec.ExitError); ok {
if ws, ok := exitError.Sys().(syscall.WaitStatus); ok {
status = ws.ExitStatus()
fields["result_code"] = status
}
}
if status != 1 {
// Combine go err + stderr output
out = strings.TrimSpace(out)
if len(out) > 0 {
acc.AddError(fmt.Errorf("host %s: %s, %s", u, out, err))
} else {
acc.AddError(fmt.Errorf("host %s: %s", u, err))
}
fields["result_code"] = 2
acc.AddFields("ping", fields, tags)
return
}
}
trans, rec, ttl, min, avg, max, stddev, err := processPingOutput(out)
if err != nil {
// fatal error
acc.AddError(fmt.Errorf("%s: %s", err, u))
fields["result_code"] = 2
acc.AddFields("ping", fields, tags)
return
}
// Calculate packet loss percentage
loss := float64(trans-rec) / float64(trans) * 100.0
fields["packets_transmitted"] = trans
fields["packets_received"] = rec
fields["percent_packet_loss"] = loss
if ttl >= 0 {
fields["ttl"] = ttl
}
if min >= 0 {
fields["minimum_response_ms"] = min
}
if avg >= 0 {
fields["average_response_ms"] = avg
}
if max >= 0 {
fields["maximum_response_ms"] = max
}
if stddev >= 0 {
fields["standard_deviation_ms"] = stddev
}
acc.AddFields("ping", fields, tags)
}
// args returns the arguments for the 'ping' executable
func (p *Ping) args(url string, system string) []string {
if len(p.Arguments) > 0 {
return append(p.Arguments, url)
}
// build the ping command args based on toml config
args := []string{"-c", strconv.Itoa(p.Count), "-n", "-s", "16"}
if p.PingInterval > 0 {
args = append(args, "-i", strconv.FormatFloat(p.PingInterval, 'f', -1, 64))
}
if p.Timeout > 0 {
switch system {
case "darwin":
args = append(args, "-W", strconv.FormatFloat(p.Timeout*1000, 'f', -1, 64))
case "freebsd", "netbsd", "openbsd":
args = append(args, "-W", strconv.FormatFloat(p.Timeout*1000, 'f', -1, 64))
case "linux":
args = append(args, "-W", strconv.FormatFloat(p.Timeout, 'f', -1, 64))
default:
// Not sure the best option here, just assume GNU ping?
args = append(args, "-W", strconv.FormatFloat(p.Timeout, 'f', -1, 64))
}
}
if p.Deadline > 0 {
switch system {
case "darwin", "freebsd", "netbsd", "openbsd":
args = append(args, "-t", strconv.Itoa(p.Deadline))
case "linux":
args = append(args, "-w", strconv.Itoa(p.Deadline))
default:
// not sure the best option here, just assume gnu ping?
args = append(args, "-w", strconv.Itoa(p.Deadline))
}
}
if p.Interface != "" {
switch system {
case "darwin":
args = append(args, "-I", p.Interface)
case "freebsd", "netbsd", "openbsd":
args = append(args, "-S", p.Interface)
case "linux":
args = append(args, "-I", p.Interface)
default:
// not sure the best option here, just assume gnu ping?
args = append(args, "-i", p.Interface)
}
}
args = append(args, url)
return args
}
// processPingOutput takes in a string output from the ping command, like:
//
// ping www.google.com (173.194.115.84): 56 data bytes
// 64 bytes from 173.194.115.84: icmp_seq=0 ttl=54 time=52.172 ms
// 64 bytes from 173.194.115.84: icmp_seq=1 ttl=54 time=34.843 ms
//
// --- www.google.com ping statistics ---
// 2 packets transmitted, 2 packets received, 0.0% packet loss
// round-trip min/avg/max/stddev = 34.843/43.508/52.172/8.664 ms
//
// It returns (<transmitted packets>, <received packets>, <average response>)
func processPingOutput(out string) (int, int, int, float64, float64, float64, float64, error) {
var trans, recv, ttl int = 0, 0, -1
var min, avg, max, stddev float64 = -1.0, -1.0, -1.0, -1.0
// Set this error to nil if we find a 'transmitted' line
err := errors.New("Fatal error processing ping output")
lines := strings.Split(out, "\n")
for _, line := range lines {
// Reading only first TTL, ignoring other TTL messages
if ttl == -1 && strings.Contains(line, "ttl=") {
ttl, err = getTTL(line)
} else if strings.Contains(line, "transmitted") &&
strings.Contains(line, "received") {
trans, recv, err = getPacketStats(line, trans, recv)
if err != nil {
return trans, recv, ttl, min, avg, max, stddev, err
}
} else if strings.Contains(line, "min/avg/max") {
min, avg, max, stddev, err = checkRoundTripTimeStats(line, min, avg, max, stddev)
if err != nil {
return trans, recv, ttl, min, avg, max, stddev, err
}
}
}
return trans, recv, ttl, min, avg, max, stddev, err
}
func getPacketStats(line string, trans, recv int) (int, int, error) {
stats := strings.Split(line, ", ")
// Transmitted packets
trans, err := strconv.Atoi(strings.Split(stats[0], " ")[0])
if err != nil {
return trans, recv, err
}
// Received packets
recv, err = strconv.Atoi(strings.Split(stats[1], " ")[0])
return trans, recv, err
}
func getTTL(line string) (int, error) {
ttlLine := regexp.MustCompile(`ttl=(\d+)`)
ttlMatch := ttlLine.FindStringSubmatch(line)
return strconv.Atoi(ttlMatch[1])
}
func checkRoundTripTimeStats(line string, min, avg, max,
stddev float64) (float64, float64, float64, float64, error) {
stats := strings.Split(line, " ")[3]
data := strings.Split(stats, "/")
min, err := strconv.ParseFloat(data[0], 64)
if err != nil {
return min, avg, max, stddev, err
}
avg, err = strconv.ParseFloat(data[1], 64)
if err != nil {
return min, avg, max, stddev, err
}
max, err = strconv.ParseFloat(data[2], 64)
if err != nil {
return min, avg, max, stddev, err
}
if len(data) == 4 {
stddev, err = strconv.ParseFloat(data[3], 64)
if err != nil {
return min, avg, max, stddev, err
}
}
return min, avg, max, stddev, err
}

View File

@ -180,12 +180,12 @@ func mockHostPinger(binary string, timeout float64, args ...string) (string, err
func TestPingGather(t *testing.T) { func TestPingGather(t *testing.T) {
var acc testutil.Accumulator var acc testutil.Accumulator
p := Ping{ p := Ping{
Urls: []string{"www.google.com", "www.reddit.com"}, Urls: []string{"localhost", "influxdata.com"},
pingHost: mockHostPinger, pingHost: mockHostPinger,
} }
acc.GatherError(p.Gather) acc.GatherError(p.Gather)
tags := map[string]string{"url": "www.google.com"} tags := map[string]string{"url": "localhost"}
fields := map[string]interface{}{ fields := map[string]interface{}{
"packets_transmitted": 5, "packets_transmitted": 5,
"packets_received": 5, "packets_received": 5,
@ -199,7 +199,7 @@ func TestPingGather(t *testing.T) {
} }
acc.AssertContainsTaggedFields(t, "ping", fields, tags) acc.AssertContainsTaggedFields(t, "ping", fields, tags)
tags = map[string]string{"url": "www.reddit.com"} tags = map[string]string{"url": "influxdata.com"}
acc.AssertContainsTaggedFields(t, "ping", fields, tags) acc.AssertContainsTaggedFields(t, "ping", fields, tags)
} }
@ -339,3 +339,20 @@ func TestPingBinary(t *testing.T) {
} }
acc.GatherError(p.Gather) acc.GatherError(p.Gather)
} }
// Test that Gather function works using native ping
func TestPingGatherNative(t *testing.T) {
if testing.Short() {
t.Skip("Skipping test due to permission requirements.")
}
var acc testutil.Accumulator
p := Ping{
Urls: []string{"localhost", "127.0.0.2"},
Method: "native",
Count: 5,
}
assert.NoError(t, acc.GatherError(p.Gather))
assert.True(t, acc.HasPoint("ping", map[string]string{"url": "localhost"}, "packets_transmitted", 5))
}

View File

@ -5,103 +5,21 @@ package ping
import ( import (
"errors" "errors"
"fmt" "fmt"
"net"
"os/exec"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"sync"
"time"
"github.com/influxdata/telegraf" "github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/inputs"
) )
// HostPinger is a function that runs the "ping" function using a list of func (p *Ping) pingToURL(u string, acc telegraf.Accumulator) {
// passed arguments. This can be easily switched with a mocked ping function
// for unit test purposes (see ping_test.go)
type HostPinger func(binary string, timeout float64, args ...string) (string, error)
type Ping struct {
wg sync.WaitGroup
// Number of pings to send (ping -c <COUNT>)
Count int
// Ping timeout, in seconds. 0 means no timeout (ping -W <TIMEOUT>)
Timeout float64
// URLs to ping
Urls []string
// Ping executable binary
Binary string
// Arguments for ping command.
// when `Arguments` is not empty, other options (ping_interval, timeout, etc) will be ignored
Arguments []string
// host ping function
pingHost HostPinger
}
func (s *Ping) Description() string {
return "Ping given url(s) and return statistics"
}
const sampleConfig = `
## List of urls to ping
urls = ["www.google.com"]
## number of pings to send per collection (ping -n <COUNT>)
# count = 1
## Ping timeout, in seconds. 0.0 means default timeout (ping -w <TIMEOUT>)
# timeout = 0.0
## Specify the ping executable binary, default is "ping"
# binary = "ping"
## Arguments for ping command
## when arguments is not empty, other options (ping_interval, timeout, etc) will be ignored
# arguments = ["-c", "3"]
`
func (s *Ping) SampleConfig() string {
return sampleConfig
}
func (p *Ping) Gather(acc telegraf.Accumulator) error {
if p.Count < 1 { if p.Count < 1 {
p.Count = 1 p.Count = 1
} }
// Spin off a go routine for each url to ping
for _, url := range p.Urls {
p.wg.Add(1)
go p.pingToURL(url, acc)
}
p.wg.Wait()
return nil
}
func (p *Ping) pingToURL(u string, acc telegraf.Accumulator) {
defer p.wg.Done()
tags := map[string]string{"url": u} tags := map[string]string{"url": u}
fields := map[string]interface{}{"result_code": 0} fields := map[string]interface{}{"result_code": 0}
_, err := net.LookupHost(u)
if err != nil {
acc.AddError(err)
fields["result_code"] = 1
acc.AddFields("ping", fields, tags)
return
}
args := p.args(u) args := p.args(u)
totalTimeout := 60.0 totalTimeout := 60.0
if len(p.Arguments) == 0 { if len(p.Arguments) == 0 {
@ -151,17 +69,6 @@ func (p *Ping) pingToURL(u string, acc telegraf.Accumulator) {
acc.AddFields("ping", fields, tags) acc.AddFields("ping", fields, tags)
} }
func hostPinger(binary string, timeout float64, args ...string) (string, error) {
bin, err := exec.LookPath(binary)
if err != nil {
return "", err
}
c := exec.Command(bin, args...)
out, err := internal.CombinedOutputTimeout(c,
time.Second*time.Duration(timeout+1))
return string(out), err
}
// args returns the arguments for the 'ping' executable // args returns the arguments for the 'ping' executable
func (p *Ping) args(url string) []string { func (p *Ping) args(url string) []string {
if len(p.Arguments) > 0 { if len(p.Arguments) > 0 {
@ -246,14 +153,3 @@ func (p *Ping) timeout() float64 {
} }
return 4 + 1 return 4 + 1
} }
func init() {
inputs.Add("ping", func() telegraf.Input {
return &Ping{
pingHost: hostPinger,
Count: 1,
Binary: "ping",
Arguments: []string{},
}
})
}