From 68e41f130cc6edcc0bc8a16851f87cdffacd1bbe Mon Sep 17 00:00:00 2001 From: Cameron Sparr Date: Wed, 2 Sep 2015 17:16:52 -0600 Subject: [PATCH] Ping plugin Closes #167 --- CHANGELOG.md | 4 +- config.go | 4 +- outputs/kafka/kafka.go | 8 +- plugins/all/all.go | 1 + plugins/ping/ping.go | 177 +++++++++++++++++++++++++++++++ plugins/ping/ping_test.go | 218 ++++++++++++++++++++++++++++++++++++++ testutil/accumulator.go | 11 ++ 7 files changed, 416 insertions(+), 7 deletions(-) create mode 100644 plugins/ping/ping.go create mode 100644 plugins/ping/ping_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 575a3c35db49a..26e307908e7cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ ## v0.1.8 [unreleased] ### Release Notes -Telegraf will now write data in UTC at second precision by default +- Telegraf will now write data in UTC at second precision by default +- Now using Go 1.5 to build telegraf ### Features - [#150](https://github.com/influxdb/telegraf/pull/150): Add Host Uptime metric to system plugin @@ -10,6 +11,7 @@ Telegraf will now write data in UTC at second precision by default - [#165](https://github.com/influxdb/telegraf/pull/165): Add additional metrics to mysql plugin. Thanks @nickscript0 - [#162](https://github.com/influxdb/telegraf/pull/162): Write UTC by default, provide option - [#166](https://github.com/influxdb/telegraf/pull/166): Upload binaries to S3 +- [#169](https://github.com/influxdb/telegraf/pull/169): Ping plugin ### Bugfixes diff --git a/config.go b/config.go index 3c97f1346c489..fae445e8cec1e 100644 --- a/config.go +++ b/config.go @@ -439,8 +439,8 @@ func PrintSampleConfig() { func PrintPluginConfig(name string) error { if creator, ok := plugins.Plugins[name]; ok { plugin := creator() - fmt.Printf("# %s\n[%s]\n", plugin.Description(), name) - fmt.Printf(strings.TrimSpace(plugin.SampleConfig())) + fmt.Printf("# %s\n[%s]", plugin.Description(), name) + fmt.Printf(plugin.SampleConfig()) } else { return errors.New(fmt.Sprintf("Plugin %s not found", name)) } diff --git a/outputs/kafka/kafka.go b/outputs/kafka/kafka.go index ac4d6116422c8..49a729b42c149 100644 --- a/outputs/kafka/kafka.go +++ b/outputs/kafka/kafka.go @@ -19,10 +19,10 @@ type Kafka struct { } var sampleConfig = ` - # URLs of kafka brokers - brokers = ["localhost:9092"] - # Kafka topic for producer messages - topic = "telegraf" + # URLs of kafka brokers + brokers = ["localhost:9092"] + # Kafka topic for producer messages + topic = "telegraf" ` func (k *Kafka) Connect() error { diff --git a/plugins/all/all.go b/plugins/all/all.go index 9400a77a1a16b..ffef12b33fcc5 100644 --- a/plugins/all/all.go +++ b/plugins/all/all.go @@ -14,6 +14,7 @@ import ( _ "github.com/influxdb/telegraf/plugins/mongodb" _ "github.com/influxdb/telegraf/plugins/mysql" _ "github.com/influxdb/telegraf/plugins/nginx" + _ "github.com/influxdb/telegraf/plugins/ping" _ "github.com/influxdb/telegraf/plugins/postgresql" _ "github.com/influxdb/telegraf/plugins/prometheus" _ "github.com/influxdb/telegraf/plugins/rabbitmq" diff --git a/plugins/ping/ping.go b/plugins/ping/ping.go new file mode 100644 index 0000000000000..fd26b8a8be415 --- /dev/null +++ b/plugins/ping/ping.go @@ -0,0 +1,177 @@ +package ping + +import ( + "errors" + "os/exec" + "strconv" + "strings" + "sync" + + "github.com/influxdb/telegraf/plugins" +) + +// 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(args ...string) (string, error) + +type Ping struct { + // Interval at which to ping (ping -i ) + PingInterval float64 `toml:"ping_interval"` + + // Number of pings to send (ping -c ) + Count int + + // Ping timeout, in seconds. 0 means no timeout (ping -t ) + Timeout float64 + + // Interface to send ping from (ping -I ) + Interface string + + // URLs to ping + Urls []string + + // host ping function + pingHost HostPinger +} + +func (_ *Ping) Description() string { + return "Ping given url(s) and return statistics" +} + +var sampleConfig = ` + # urls to ping + urls = ["www.google.com"] # required + # number of pings to send (ping -c ) + count = 1 # required + # interval, in s, at which to ping. 0 == default (ping -i ) + ping_interval = 0.0 + # ping timeout, in s. 0 == no timeout (ping -t ) + timeout = 0.0 + # interface to send ping from (ping -I ) + interface = "" +` + +func (_ *Ping) SampleConfig() string { + return sampleConfig +} + +func (p *Ping) Gather(acc plugins.Accumulator) error { + + var wg sync.WaitGroup + errorChannel := make(chan error, len(p.Urls)*2) + + // Spin off a go routine for each url to ping + for _, url := range p.Urls { + wg.Add(1) + go func(url string, acc plugins.Accumulator) { + defer wg.Done() + args := p.args(url) + out, err := p.pingHost(args...) + if err != nil { + // Combine go err + stderr output + errorChannel <- errors.New( + strings.TrimSpace(out) + ", " + err.Error()) + } + tags := map[string]string{"url": url} + trans, rec, avg, err := processPingOutput(out) + if err != nil { + // fatal error + errorChannel <- err + return + } + // Calculate packet loss percentage + loss := float64(trans-rec) / float64(trans) * 100.0 + acc.Add("packets_transmitted", trans, tags) + acc.Add("packets_received", rec, tags) + acc.Add("percent_packet_loss", loss, tags) + acc.Add("average_response_ms", avg, tags) + }(url, acc) + } + + 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 hostPinger(args ...string) (string, error) { + c := exec.Command("ping", args...) + out, err := c.CombinedOutput() + return string(out), err +} + +// args returns the arguments for the 'ping' executable +func (p *Ping) args(url string) []string { + // Build the ping command args based on toml config + args := []string{"-c", strconv.Itoa(p.Count)} + if p.PingInterval > 0 { + args = append(args, "-i", strconv.FormatFloat(p.PingInterval, 'f', 1, 64)) + } + if p.Timeout > 0 { + args = append(args, "-t", strconv.FormatFloat(p.Timeout, 'f', 1, 64)) + } + if p.Interface != "" { + 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 (, , ) +func processPingOutput(out string) (int, int, float64, error) { + var trans, recv int + var avg float64 + // 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 { + if strings.Contains(line, "transmitted") && + strings.Contains(line, "received") { + err = nil + stats := strings.Split(line, ", ") + // Transmitted packets + trans, err = strconv.Atoi(strings.Split(stats[0], " ")[0]) + if err != nil { + return trans, recv, avg, err + } + // Received packets + recv, err = strconv.Atoi(strings.Split(stats[1], " ")[0]) + if err != nil { + return trans, recv, avg, err + } + } else if strings.Contains(line, "min/avg/max") { + stats := strings.Split(line, " = ")[1] + avg, err = strconv.ParseFloat(strings.Split(stats, "/")[1], 64) + if err != nil { + return trans, recv, avg, err + } + } + } + return trans, recv, avg, err +} + +func init() { + plugins.Add("ping", func() plugins.Plugin { + return &Ping{pingHost: hostPinger} + }) +} diff --git a/plugins/ping/ping_test.go b/plugins/ping/ping_test.go new file mode 100644 index 0000000000000..5fed0b6c86987 --- /dev/null +++ b/plugins/ping/ping_test.go @@ -0,0 +1,218 @@ +package ping + +import ( + "errors" + "reflect" + "sort" + "testing" + + "github.com/influxdb/telegraf/testutil" + "github.com/stretchr/testify/assert" +) + +// BSD/Darwin ping output +var bsdPingOutput = ` +PING www.google.com (216.58.217.36): 56 data bytes +64 bytes from 216.58.217.36: icmp_seq=0 ttl=55 time=15.087 ms +64 bytes from 216.58.217.36: icmp_seq=1 ttl=55 time=21.564 ms +64 bytes from 216.58.217.36: icmp_seq=2 ttl=55 time=27.263 ms +64 bytes from 216.58.217.36: icmp_seq=3 ttl=55 time=18.828 ms +64 bytes from 216.58.217.36: icmp_seq=4 ttl=55 time=18.378 ms + +--- www.google.com ping statistics --- +5 packets transmitted, 5 packets received, 0.0% packet loss +round-trip min/avg/max/stddev = 15.087/20.224/27.263/4.076 ms +` + +// Linux ping output +var linuxPingOutput = ` +PING www.google.com (216.58.218.164) 56(84) bytes of data. +64 bytes from host.net (216.58.218.164): icmp_seq=1 ttl=63 time=35.2 ms +64 bytes from host.net (216.58.218.164): icmp_seq=2 ttl=63 time=42.3 ms +64 bytes from host.net (216.58.218.164): icmp_seq=3 ttl=63 time=45.1 ms +64 bytes from host.net (216.58.218.164): icmp_seq=4 ttl=63 time=43.5 ms +64 bytes from host.net (216.58.218.164): icmp_seq=5 ttl=63 time=51.8 ms + +--- www.google.com ping statistics --- +5 packets transmitted, 5 received, 0% packet loss, time 4010ms +rtt min/avg/max/mdev = 35.225/43.628/51.806/5.325 ms +` + +// Fatal ping output (invalid argument) +var fatalPingOutput = ` +ping: -i interval too short: Operation not permitted +` + +// Test that ping command output is processed properly +func TestProcessPingOutput(t *testing.T) { + trans, rec, avg, err := processPingOutput(bsdPingOutput) + assert.NoError(t, err) + assert.Equal(t, 5, trans, "5 packets were transmitted") + assert.Equal(t, 5, rec, "5 packets were transmitted") + assert.InDelta(t, 20.224, avg, 0.001) + + trans, rec, avg, err = processPingOutput(linuxPingOutput) + assert.NoError(t, err) + assert.Equal(t, 5, trans, "5 packets were transmitted") + assert.Equal(t, 5, rec, "5 packets were transmitted") + assert.InDelta(t, 43.628, avg, 0.001) +} + +// Test that processPingOutput returns an error when 'ping' fails to run, such +// as when an invalid argument is provided +func TestErrorProcessPingOutput(t *testing.T) { + _, _, _, err := processPingOutput(fatalPingOutput) + assert.Error(t, err, "Error was expected from processPingOutput") +} + +// Test that arg lists and created correctly +func TestArgs(t *testing.T) { + p := Ping{ + Count: 2, + } + + // Actual and Expected arg lists must be sorted for reflect.DeepEqual + + actual := p.args("www.google.com") + expected := []string{"-c", "2", "www.google.com"} + sort.Strings(actual) + sort.Strings(expected) + assert.True(t, reflect.DeepEqual(expected, actual), + "Expected: %s Actual: %s", expected, actual) + + p.Interface = "eth0" + actual = p.args("www.google.com") + expected = []string{"-c", "2", "-I", "eth0", "www.google.com"} + sort.Strings(actual) + sort.Strings(expected) + assert.True(t, reflect.DeepEqual(expected, actual), + "Expected: %s Actual: %s", expected, actual) + + p.Timeout = 12.0 + actual = p.args("www.google.com") + expected = []string{"-c", "2", "-I", "eth0", "-t", "12.0", "www.google.com"} + sort.Strings(actual) + sort.Strings(expected) + assert.True(t, reflect.DeepEqual(expected, actual), + "Expected: %s Actual: %s", expected, actual) + + p.PingInterval = 1.2 + actual = p.args("www.google.com") + expected = []string{"-c", "2", "-I", "eth0", "-t", "12.0", "-i", "1.2", + "www.google.com"} + sort.Strings(actual) + sort.Strings(expected) + assert.True(t, reflect.DeepEqual(expected, actual), + "Expected: %s Actual: %s", expected, actual) +} + +func mockHostPinger(args ...string) (string, error) { + return linuxPingOutput, 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"} + assert.NoError(t, acc.ValidateTaggedValue("packets_transmitted", 5, tags)) + assert.NoError(t, acc.ValidateTaggedValue("packets_received", 5, tags)) + assert.NoError(t, acc.ValidateTaggedValue("percent_packet_loss", 0.0, tags)) + assert.NoError(t, acc.ValidateTaggedValue("average_response_ms", + 43.628, tags)) + + tags = map[string]string{"url": "www.reddit.com"} + assert.NoError(t, acc.ValidateTaggedValue("packets_transmitted", 5, tags)) + assert.NoError(t, acc.ValidateTaggedValue("packets_received", 5, tags)) + assert.NoError(t, acc.ValidateTaggedValue("percent_packet_loss", 0.0, tags)) + assert.NoError(t, acc.ValidateTaggedValue("average_response_ms", + 43.628, tags)) +} + +var lossyPingOutput = ` +PING www.google.com (216.58.218.164) 56(84) bytes of data. +64 bytes from host.net (216.58.218.164): icmp_seq=1 ttl=63 time=35.2 ms +64 bytes from host.net (216.58.218.164): icmp_seq=3 ttl=63 time=45.1 ms +64 bytes from host.net (216.58.218.164): icmp_seq=5 ttl=63 time=51.8 ms + +--- www.google.com ping statistics --- +5 packets transmitted, 3 received, 40% packet loss, time 4010ms +rtt min/avg/max/mdev = 35.225/44.033/51.806/5.325 ms +` + +func mockLossyHostPinger(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"} + assert.NoError(t, acc.ValidateTaggedValue("packets_transmitted", 5, tags)) + assert.NoError(t, acc.ValidateTaggedValue("packets_received", 3, tags)) + assert.NoError(t, acc.ValidateTaggedValue("percent_packet_loss", 40.0, tags)) + assert.NoError(t, acc.ValidateTaggedValue("average_response_ms", 44.033, tags)) +} + +var errorPingOutput = ` +PING www.amazon.com (176.32.98.166): 56 data bytes +Request timeout for icmp_seq 0 + +--- www.amazon.com ping statistics --- +2 packets transmitted, 0 packets received, 100.0% packet loss +` + +func mockErrorHostPinger(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"} + assert.NoError(t, acc.ValidateTaggedValue("packets_transmitted", 2, tags)) + assert.NoError(t, acc.ValidateTaggedValue("packets_received", 0, tags)) + assert.NoError(t, acc.ValidateTaggedValue("percent_packet_loss", 100.0, tags)) + assert.NoError(t, acc.ValidateTaggedValue("average_response_ms", 0.0, tags)) +} + +func mockFatalHostPinger(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") +} diff --git a/testutil/accumulator.go b/testutil/accumulator.go index db3a67e664af1..3d9d408277609 100644 --- a/testutil/accumulator.go +++ b/testutil/accumulator.go @@ -151,3 +151,14 @@ func (a *Accumulator) HasFloatValue(measurement string) bool { return false } + +// HasMeasurement returns true if the accumulator has a measurement with the +// given name +func (a *Accumulator) HasMeasurement(measurement string) bool { + for _, p := range a.Points { + if p.Measurement == measurement { + return true + } + } + return false +}