Ping windows (#1532)
* Ping for windows * En ping output * Code format * Code review * Default timeout * Fix problem with std error when no data received ( exit status = 1 )
This commit is contained in:
parent
e68f251df7
commit
0b3958d3cd
|
@ -1,3 +1,210 @@
|
||||||
// +build windows
|
// +build windows
|
||||||
|
|
||||||
package ping
|
package ping
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/influxdata/telegraf"
|
||||||
|
"github.com/influxdata/telegraf/internal"
|
||||||
|
"github.com/influxdata/telegraf/plugins/inputs"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 Ping struct {
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// host ping function
|
||||||
|
pingHost HostPinger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Ping) Description() string {
|
||||||
|
return "Ping given url(s) and return statistics"
|
||||||
|
}
|
||||||
|
|
||||||
|
const sampleConfig = `
|
||||||
|
## urls to ping
|
||||||
|
urls = ["www.google.com"] # required
|
||||||
|
|
||||||
|
## number of pings to send per collection (ping -n <COUNT>)
|
||||||
|
count = 4 # required
|
||||||
|
|
||||||
|
## Ping timeout, in seconds. 0 means default timeout (ping -w <TIMEOUT>)
|
||||||
|
Timeout = 0
|
||||||
|
`
|
||||||
|
|
||||||
|
func (s *Ping) SampleConfig() string {
|
||||||
|
return sampleConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostPinger(timeout float64, args ...string) (string, error) {
|
||||||
|
bin, err := exec.LookPath("ping")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// processPingOutput takes in a string output from the ping command
|
||||||
|
// based on linux implementation but using regex ( multilanguage support ) ( shouldn't affect the performance of the program )
|
||||||
|
// It returns (<transmitted packets>, <received packets>, <average response>, <min response>, <max response>)
|
||||||
|
func processPingOutput(out string) (int, int, int, int, int, error) {
|
||||||
|
// So find a line contain 3 numbers except reply lines
|
||||||
|
var stats, aproxs []string = nil, nil
|
||||||
|
err := errors.New("Fatal error processing ping output")
|
||||||
|
stat := regexp.MustCompile(`=\W*(\d+)\D*=\W*(\d+)\D*=\W*(\d+)`)
|
||||||
|
aprox := regexp.MustCompile(`=\W*(\d+)\D*ms\D*=\W*(\d+)\D*ms\D*=\W*(\d+)\D*ms`)
|
||||||
|
lines := strings.Split(out, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if !strings.Contains(line, "TTL") {
|
||||||
|
if stats == nil {
|
||||||
|
stats = stat.FindStringSubmatch(line)
|
||||||
|
}
|
||||||
|
if stats != nil && aproxs == nil {
|
||||||
|
aproxs = aprox.FindStringSubmatch(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stats data should contain 4 members: entireExpression + ( Send, Receive, Lost )
|
||||||
|
if len(stats) != 4 {
|
||||||
|
return 0, 0, 0, 0, 0, err
|
||||||
|
}
|
||||||
|
trans, err := strconv.Atoi(stats[1])
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, 0, 0, err
|
||||||
|
}
|
||||||
|
rec, err := strconv.Atoi(stats[2])
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// aproxs data should contain 4 members: entireExpression + ( min, max, avg )
|
||||||
|
if len(aproxs) != 4 {
|
||||||
|
return trans, rec, 0, 0, 0, err
|
||||||
|
}
|
||||||
|
min, err := strconv.Atoi(aproxs[1])
|
||||||
|
if err != nil {
|
||||||
|
return trans, rec, 0, 0, 0, err
|
||||||
|
}
|
||||||
|
max, err := strconv.Atoi(aproxs[2])
|
||||||
|
if err != nil {
|
||||||
|
return trans, rec, 0, 0, 0, err
|
||||||
|
}
|
||||||
|
avg, err := strconv.Atoi(aproxs[3])
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return trans, rec, avg, min, max, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Ping) timeout() float64 {
|
||||||
|
// According to MSDN, default ping timeout for windows is 4 second
|
||||||
|
// Add also one second interval
|
||||||
|
|
||||||
|
if p.Timeout > 0 {
|
||||||
|
return p.Timeout + 1
|
||||||
|
}
|
||||||
|
return 4 + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// args returns the arguments for the 'ping' executable
|
||||||
|
func (p *Ping) args(url string) []string {
|
||||||
|
args := []string{"-n", strconv.Itoa(p.Count)}
|
||||||
|
|
||||||
|
if p.Timeout > 0 {
|
||||||
|
args = append(args, "-w", strconv.FormatFloat(p.Timeout*1000, 'f', 0, 64))
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, url)
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Ping) Gather(acc telegraf.Accumulator) error {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errorChannel := make(chan error, len(p.Urls)*2)
|
||||||
|
var pendingError error = nil
|
||||||
|
// 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()
|
||||||
|
args := p.args(u)
|
||||||
|
totalTimeout := p.timeout() * float64(p.Count)
|
||||||
|
out, err := p.pingHost(totalTimeout, args...)
|
||||||
|
// ping host return exitcode != 0 also when there was no response from host
|
||||||
|
// but command was execute succesfully
|
||||||
|
if err != nil {
|
||||||
|
// Combine go err + stderr output
|
||||||
|
pendingError = errors.New(strings.TrimSpace(out) + ", " + err.Error())
|
||||||
|
}
|
||||||
|
tags := map[string]string{"url": u}
|
||||||
|
trans, rec, avg, min, max, err := processPingOutput(out)
|
||||||
|
if err != nil {
|
||||||
|
// fatal error
|
||||||
|
if pendingError != nil {
|
||||||
|
errorChannel <- pendingError
|
||||||
|
}
|
||||||
|
errorChannel <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Calculate packet loss percentage
|
||||||
|
loss := float64(trans-rec) / float64(trans) * 100.0
|
||||||
|
fields := map[string]interface{}{
|
||||||
|
"packets_transmitted": trans,
|
||||||
|
"packets_received": rec,
|
||||||
|
"percent_packet_loss": loss,
|
||||||
|
}
|
||||||
|
if avg > 0 {
|
||||||
|
fields["average_response_ms"] = avg
|
||||||
|
}
|
||||||
|
if min > 0 {
|
||||||
|
fields["minimum_response_ms"] = min
|
||||||
|
}
|
||||||
|
if max > 0 {
|
||||||
|
fields["maximum_response_ms"] = max
|
||||||
|
}
|
||||||
|
acc.AddFields("ping", fields, tags)
|
||||||
|
}(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(errorChannel)
|
||||||
|
|
||||||
|
// Get all errors and return them as one giant error
|
||||||
|
errorStrings := []string{}
|
||||||
|
for err := range errorChannel {
|
||||||
|
errorStrings = append(errorStrings, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errorStrings) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New(strings.Join(errorStrings, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
inputs.Add("ping", func() telegraf.Input {
|
||||||
|
return &Ping{pingHost: hostPinger}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,218 @@
|
||||||
|
// +build windows
|
||||||
|
package ping
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/influxdata/telegraf/testutil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Windows ping format ( should support multilanguage ?)
|
||||||
|
var winPLPingOutput = `
|
||||||
|
Badanie 8.8.8.8 z 32 bajtami danych:
|
||||||
|
Odpowiedz z 8.8.8.8: bajtow=32 czas=49ms TTL=43
|
||||||
|
Odpowiedz z 8.8.8.8: bajtow=32 czas=46ms TTL=43
|
||||||
|
Odpowiedz z 8.8.8.8: bajtow=32 czas=48ms TTL=43
|
||||||
|
Odpowiedz z 8.8.8.8: bajtow=32 czas=57ms TTL=43
|
||||||
|
|
||||||
|
Statystyka badania ping dla 8.8.8.8:
|
||||||
|
Pakiety: Wyslane = 4, Odebrane = 4, Utracone = 0
|
||||||
|
(0% straty),
|
||||||
|
Szacunkowy czas bladzenia pakietww w millisekundach:
|
||||||
|
Minimum = 46 ms, Maksimum = 57 ms, Czas sredni = 50 ms
|
||||||
|
`
|
||||||
|
|
||||||
|
// Windows ping format ( should support multilanguage ?)
|
||||||
|
var winENPingOutput = `
|
||||||
|
Pinging 8.8.8.8 with 32 bytes of data:
|
||||||
|
Reply from 8.8.8.8: bytes=32 time=52ms TTL=43
|
||||||
|
Reply from 8.8.8.8: bytes=32 time=50ms TTL=43
|
||||||
|
Reply from 8.8.8.8: bytes=32 time=50ms TTL=43
|
||||||
|
Reply from 8.8.8.8: bytes=32 time=51ms TTL=43
|
||||||
|
|
||||||
|
Ping statistics for 8.8.8.8:
|
||||||
|
Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
|
||||||
|
Approximate round trip times in milli-seconds:
|
||||||
|
Minimum = 50ms, Maximum = 52ms, Average = 50ms
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestHost(t *testing.T) {
|
||||||
|
trans, rec, avg, min, max, err := processPingOutput(winPLPingOutput)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 4, trans, "4 packets were transmitted")
|
||||||
|
assert.Equal(t, 4, rec, "4 packets were received")
|
||||||
|
assert.Equal(t, 50, avg, "Average 50")
|
||||||
|
assert.Equal(t, 46, min, "Min 46")
|
||||||
|
assert.Equal(t, 57, max, "max 57")
|
||||||
|
|
||||||
|
trans, rec, avg, min, max, err = processPingOutput(winENPingOutput)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 4, trans, "4 packets were transmitted")
|
||||||
|
assert.Equal(t, 4, rec, "4 packets were received")
|
||||||
|
assert.Equal(t, 50, avg, "Average 50")
|
||||||
|
assert.Equal(t, 50, min, "Min 50")
|
||||||
|
assert.Equal(t, 52, max, "Max 52")
|
||||||
|
}
|
||||||
|
|
||||||
|
func mockHostPinger(timeout float64, args ...string) (string, error) {
|
||||||
|
return winENPingOutput, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that Gather function works on a normal ping
|
||||||
|
func TestPingGather(t *testing.T) {
|
||||||
|
var acc testutil.Accumulator
|
||||||
|
p := Ping{
|
||||||
|
Urls: []string{"www.google.com", "www.reddit.com"},
|
||||||
|
pingHost: mockHostPinger,
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Gather(&acc)
|
||||||
|
tags := map[string]string{"url": "www.google.com"}
|
||||||
|
fields := map[string]interface{}{
|
||||||
|
"packets_transmitted": 4,
|
||||||
|
"packets_received": 4,
|
||||||
|
"percent_packet_loss": 0.0,
|
||||||
|
"average_response_ms": 50,
|
||||||
|
"minimum_response_ms": 50,
|
||||||
|
"maximum_response_ms": 52,
|
||||||
|
}
|
||||||
|
acc.AssertContainsTaggedFields(t, "ping", fields, tags)
|
||||||
|
|
||||||
|
tags = map[string]string{"url": "www.reddit.com"}
|
||||||
|
acc.AssertContainsTaggedFields(t, "ping", fields, tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorPingOutput = `
|
||||||
|
Badanie nask.pl [195.187.242.157] z 32 bajtami danych:
|
||||||
|
Upłynął limit czasu żądania.
|
||||||
|
Upłynął limit czasu żądania.
|
||||||
|
Upłynął limit czasu żądania.
|
||||||
|
Upłynął limit czasu żądania.
|
||||||
|
|
||||||
|
Statystyka badania ping dla 195.187.242.157:
|
||||||
|
Pakiety: Wysłane = 4, Odebrane = 0, Utracone = 4
|
||||||
|
(100% straty),
|
||||||
|
`
|
||||||
|
|
||||||
|
func mockErrorHostPinger(timeout float64, args ...string) (string, error) {
|
||||||
|
return errorPingOutput, errors.New("No packets received")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that Gather works on a ping with no transmitted packets, even though the
|
||||||
|
// command returns an error
|
||||||
|
func TestBadPingGather(t *testing.T) {
|
||||||
|
var acc testutil.Accumulator
|
||||||
|
p := Ping{
|
||||||
|
Urls: []string{"www.amazon.com"},
|
||||||
|
pingHost: mockErrorHostPinger,
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Gather(&acc)
|
||||||
|
tags := map[string]string{"url": "www.amazon.com"}
|
||||||
|
fields := map[string]interface{}{
|
||||||
|
"packets_transmitted": 4,
|
||||||
|
"packets_received": 0,
|
||||||
|
"percent_packet_loss": 100.0,
|
||||||
|
}
|
||||||
|
acc.AssertContainsTaggedFields(t, "ping", fields, tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lossyPingOutput = `
|
||||||
|
Badanie thecodinglove.com [66.6.44.4] z 9800 bajtami danych:
|
||||||
|
Upłynął limit czasu żądania.
|
||||||
|
Odpowiedź z 66.6.44.4: bajtów=9800 czas=114ms TTL=48
|
||||||
|
Odpowiedź z 66.6.44.4: bajtów=9800 czas=114ms TTL=48
|
||||||
|
Odpowiedź z 66.6.44.4: bajtów=9800 czas=118ms TTL=48
|
||||||
|
Odpowiedź z 66.6.44.4: bajtów=9800 czas=114ms TTL=48
|
||||||
|
Odpowiedź z 66.6.44.4: bajtów=9800 czas=114ms TTL=48
|
||||||
|
Upłynął limit czasu żądania.
|
||||||
|
Odpowiedź z 66.6.44.4: bajtów=9800 czas=119ms TTL=48
|
||||||
|
Odpowiedź z 66.6.44.4: bajtów=9800 czas=116ms TTL=48
|
||||||
|
|
||||||
|
Statystyka badania ping dla 66.6.44.4:
|
||||||
|
Pakiety: Wysłane = 9, Odebrane = 7, Utracone = 2
|
||||||
|
(22% straty),
|
||||||
|
Szacunkowy czas błądzenia pakietów w millisekundach:
|
||||||
|
Minimum = 114 ms, Maksimum = 119 ms, Czas średni = 115 ms
|
||||||
|
`
|
||||||
|
|
||||||
|
func mockLossyHostPinger(timeout float64, args ...string) (string, error) {
|
||||||
|
return lossyPingOutput, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that Gather works on a ping with lossy packets
|
||||||
|
func TestLossyPingGather(t *testing.T) {
|
||||||
|
var acc testutil.Accumulator
|
||||||
|
p := Ping{
|
||||||
|
Urls: []string{"www.google.com"},
|
||||||
|
pingHost: mockLossyHostPinger,
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Gather(&acc)
|
||||||
|
tags := map[string]string{"url": "www.google.com"}
|
||||||
|
fields := map[string]interface{}{
|
||||||
|
"packets_transmitted": 9,
|
||||||
|
"packets_received": 7,
|
||||||
|
"percent_packet_loss": 22.22222222222222,
|
||||||
|
"average_response_ms": 115,
|
||||||
|
"minimum_response_ms": 114,
|
||||||
|
"maximum_response_ms": 119,
|
||||||
|
}
|
||||||
|
acc.AssertContainsTaggedFields(t, "ping", fields, tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatal ping output (invalid argument)
|
||||||
|
var fatalPingOutput = `
|
||||||
|
Bad option -d.
|
||||||
|
|
||||||
|
|
||||||
|
Usage: ping [-t] [-a] [-n count] [-l size] [-f] [-i TTL] [-v TOS]
|
||||||
|
[-r count] [-s count] [[-j host-list] | [-k host-list]]
|
||||||
|
[-w timeout] [-R] [-S srcaddr] [-4] [-6] target_name
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-t Ping the specified host until stopped.
|
||||||
|
To see statistics and continue - type Control-Break;
|
||||||
|
To stop - type Control-C.
|
||||||
|
-a Resolve addresses to hostnames.
|
||||||
|
-n count Number of echo requests to send.
|
||||||
|
-l size Send buffer size.
|
||||||
|
-f Set Don't Fragment flag in packet (IPv4-only).
|
||||||
|
-i TTL Time To Live.
|
||||||
|
-v TOS Type Of Service (IPv4-only. This setting has been deprecated
|
||||||
|
and has no effect on the type of service field in the IP Header).
|
||||||
|
-r count Record route for count hops (IPv4-only).
|
||||||
|
-s count Timestamp for count hops (IPv4-only).
|
||||||
|
-j host-list Loose source route along host-list (IPv4-only).
|
||||||
|
-k host-list Strict source route along host-list (IPv4-only).
|
||||||
|
-w timeout Timeout in milliseconds to wait for each reply.
|
||||||
|
-R Use routing header to test reverse route also (IPv6-only).
|
||||||
|
-S srcaddr Source address to use.
|
||||||
|
-4 Force using IPv4.
|
||||||
|
-6 Force using IPv6.
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
func mockFatalHostPinger(timeout float64, args ...string) (string, error) {
|
||||||
|
return fatalPingOutput, errors.New("So very bad")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that a fatal ping command does not gather any statistics.
|
||||||
|
func TestFatalPingGather(t *testing.T) {
|
||||||
|
var acc testutil.Accumulator
|
||||||
|
p := Ping{
|
||||||
|
Urls: []string{"www.amazon.com"},
|
||||||
|
pingHost: mockFatalHostPinger,
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Gather(&acc)
|
||||||
|
assert.False(t, acc.HasMeasurement("packets_transmitted"),
|
||||||
|
"Fatal ping should not have packet measurements")
|
||||||
|
assert.False(t, acc.HasMeasurement("packets_received"),
|
||||||
|
"Fatal ping should not have packet measurements")
|
||||||
|
assert.False(t, acc.HasMeasurement("percent_packet_loss"),
|
||||||
|
"Fatal ping should not have packet measurements")
|
||||||
|
assert.False(t, acc.HasMeasurement("average_response_ms"),
|
||||||
|
"Fatal ping should not have packet measurements")
|
||||||
|
}
|
Loading…
Reference in New Issue