Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support IPv6 in the ping plugin #4703

Merged
merged 26 commits into from
Oct 2, 2018
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 109 additions & 81 deletions plugins/inputs/ping/ping.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ import (
// HostPinger is a function that runs the "ping" function using a list of
// passed arguments. This can be easily switched with a mocked ping function
// for unit test purposes (see ping_test.go)
type HostPinger func(timeout float64, args ...string) (string, error)
type HostPinger func(binary string, timeout float64, args ...string) (string, error)

type Ping struct {
wg sync.WaitGroup

// Interval at which to ping (ping -i <INTERVAL>)
PingInterval float64 `toml:"ping_interval"`

Expand All @@ -43,6 +45,13 @@ type Ping struct {
// 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
}
Expand Down Expand Up @@ -71,97 +80,106 @@ const sampleConfig = `
## Interface or source address to send ping from (ping -I <INTERFACE/SRC_ADDR>)
## on Darwin and Freebsd only source address possible: (ping -S <SRC_ADDR>)
# interface = ""

## 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 (_ *Ping) SampleConfig() string {
return sampleConfig
}

func (p *Ping) Gather(acc telegraf.Accumulator) error {

var wg sync.WaitGroup

// Spin off a go routine for each url to ping
for _, url := range p.Urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
tags := map[string]string{"url": u}
fields := map[string]interface{}{"result_code": 0}
p.wg.Add(1)
p.pingToURL(url, acc)
Jaeyo marked this conversation as resolved.
Show resolved Hide resolved
}

_, err := net.LookupHost(u)
if err != nil {
acc.AddError(err)
fields["result_code"] = 1
acc.AddFields("ping", fields, tags)
return
}
p.wg.Wait()

args := p.args(u, runtime.GOOS)
totalTimeout := float64(p.Count)*p.Timeout + float64(p.Count-1)*p.PingInterval
return nil
}

out, err := p.pingHost(totalTimeout, args...)
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()
}
}
func (p *Ping) pingToURL(u string, acc telegraf.Accumulator) {
defer p.wg.Done()
tags := map[string]string{"url": u}
fields := map[string]interface{}{"result_code": 0}

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
}
}
_, err := net.LookupHost(u)
if err != nil {
acc.AddError(err)
fields["result_code"] = 1
acc.AddFields("ping", fields, tags)
return
}

trans, rec, 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 min >= 0 {
fields["minimum_response_ms"] = min
}
if avg >= 0 {
fields["average_response_ms"] = avg
}
if max >= 0 {
fields["maximum_response_ms"] = max
args := p.args(u, runtime.GOOS)
totalTimeout := 60.0
if len(p.Arguments) == 0 {
totalTimeout = float64(p.Count)*p.Timeout + float64(p.Count-1)*p.PingInterval
}

out, err := p.pingHost(p.Binary, totalTimeout, args...)
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()
}
if stddev >= 0 {
fields["standard_deviation_ms"] = stddev
}

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)
}(url)
return
}
}

wg.Wait()

return nil
trans, rec, 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 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(timeout float64, args ...string) (string, error) {
bin, err := exec.LookPath("ping")
func hostPinger(binary string, timeout float64, args ...string) (string, error) {
bin, err := exec.LookPath(binary)
if err != nil {
return "", err
}
Expand All @@ -173,19 +191,25 @@ func hostPinger(timeout float64, args ...string) (string, error) {

// args returns the arguments for the 'ping' executable
func (p *Ping) args(url string, system string) []string {
// Build the ping command args based on toml config
if len(p.Arguments) > 0 {
return p.Arguments
}

// 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", "freebsd", "netbsd", "openbsd":
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?
// not sure the best option here, just assume gnu ping?
Jaeyo marked this conversation as resolved.
Show resolved Hide resolved
args = append(args, "-W", strconv.FormatFloat(p.Timeout, 'f', -1, 64))
}
}
Expand All @@ -196,28 +220,30 @@ func (p *Ping) args(url string, system string) []string {
case "linux":
args = append(args, "-w", strconv.Itoa(p.Deadline))
default:
// Not sure the best option here, just assume GNU ping?
// 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", "freebsd", "netbsd", "openbsd":
args = append(args, "-S", p.Interface)
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)
// 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:
// processpingoutput takes in a string output from the ping command, like:
Jaeyo marked this conversation as resolved.
Show resolved Hide resolved
//
// PING www.google.com (173.194.115.84): 56 data bytes
// 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
//
Expand Down Expand Up @@ -280,6 +306,8 @@ func init() {
Count: 1,
Timeout: 1.0,
Deadline: 10,
Binary: "ping",
Arguments: []string{},
}
})
}
44 changes: 37 additions & 7 deletions plugins/inputs/ping/ping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ func TestArgs(t *testing.T) {
system string
output []string
}{
{"darwin", []string{"-c", "2", "-n", "-s", "16", "-i", "1.2", "-W", "12000", "-t", "24", "-S", "eth0", "www.google.com"}},
{"darwin", []string{"-c", "2", "-n", "-s", "16", "-i", "1.2", "-W", "12000", "-t", "24", "-I", "eth0", "www.google.com"}},
{"linux", []string{"-c", "2", "-n", "-s", "16", "-i", "1.2", "-W", "12", "-w", "24", "-I", "eth0", "www.google.com"}},
{"anything else", []string{"-c", "2", "-n", "-s", "16", "-i", "1.2", "-W", "12", "-w", "24", "-I", "eth0", "www.google.com"}},
{"anything else", []string{"-c", "2", "-n", "-s", "16", "-i", "1.2", "-W", "12", "-w", "24", "-i", "eth0", "www.google.com"}},
}
for i := range systemCases {
actual := p.args("www.google.com", systemCases[i].system)
Expand All @@ -124,7 +124,24 @@ func TestArgs(t *testing.T) {
}
}

func mockHostPinger(timeout float64, args ...string) (string, error) {
func TestArguments(t *testing.T) {
arguments := []string{"-c", "3"}
p := Ping{
Count: 2,
Interface: "eth0",
Timeout: 12.0,
Deadline: 24,
PingInterval: 1.2,
Arguments: arguments,
}

for _, system := range []string{"darwin", "linux", "anything else"} {
actual := p.args("www.google.com", system)
require.True(t, reflect.DeepEqual(actual, arguments), "Expected: %s Actual: %s", arguments, actual)
}
}

func mockHostPinger(binary string, timeout float64, args ...string) (string, error) {
return linuxPingOutput, nil
}

Expand Down Expand Up @@ -165,7 +182,7 @@ PING www.google.com (216.58.218.164) 56(84) bytes of data.
rtt min/avg/max/mdev = 35.225/44.033/51.806/5.325 ms
`

func mockLossyHostPinger(timeout float64, args ...string) (string, error) {
func mockLossyHostPinger(binary string, timeout float64, args ...string) (string, error) {
return lossyPingOutput, nil
}

Expand Down Expand Up @@ -200,7 +217,7 @@ Request timeout for icmp_seq 0
2 packets transmitted, 0 packets received, 100.0% packet loss
`

func mockErrorHostPinger(timeout float64, args ...string) (string, error) {
func mockErrorHostPinger(binary string, timeout float64, args ...string) (string, error) {
// This error will not trigger correct error paths
return errorPingOutput, nil
}
Expand All @@ -225,7 +242,7 @@ func TestBadPingGather(t *testing.T) {
acc.AssertContainsTaggedFields(t, "ping", fields, tags)
}

func mockFatalHostPinger(timeout float64, args ...string) (string, error) {
func mockFatalHostPinger(binary string, timeout float64, args ...string) (string, error) {
return fatalPingOutput, errors.New("So very bad")
}

Expand Down Expand Up @@ -265,7 +282,7 @@ func TestErrorWithHostNamePingGather(t *testing.T) {
var acc testutil.Accumulator
p := Ping{
Urls: []string{"www.amazon.com"},
pingHost: func(timeout float64, args ...string) (string, error) {
pingHost: func(binary string, timeout float64, args ...string) (string, error) {
return param.out, errors.New("So very bad")
},
}
Expand All @@ -274,3 +291,16 @@ func TestErrorWithHostNamePingGather(t *testing.T) {
assert.Contains(t, acc.Errors, param.error)
}
}

func TestPingBinary(t *testing.T) {
var acc testutil.Accumulator
p := Ping{
Urls: []string{"www.google.com"},
Binary: "ping6",
pingHost: func(binary string, timeout float64, args ...string) (string, error) {
assert.True(t, binary == "ping6")
return "", nil
},
}
acc.GatherError(p.Gather)
}
Loading