2022-05-24 21:49:47 +08:00
//go:generate ../../../tools/readme_config_includer/generator
2015-09-03 07:16:52 +08:00
package ping
import (
2022-05-24 21:49:47 +08:00
_ "embed"
2015-09-03 07:16:52 +08:00
"errors"
2020-12-10 01:54:51 +08:00
"fmt"
2019-07-12 06:07:58 +08:00
"math"
2021-01-29 01:30:00 +08:00
"net"
2021-01-28 23:54:29 +08:00
"os/exec"
2016-02-03 09:43:03 +08:00
"runtime"
2021-02-10 00:50:57 +08:00
"sort"
2019-10-23 07:46:57 +08:00
"strings"
2015-09-03 07:16:52 +08:00
"sync"
2016-04-29 09:23:45 +08:00
"time"
2015-09-03 07:16:52 +08:00
2022-09-27 03:04:30 +08:00
ping "github.com/prometheus-community/pro-bing"
2021-11-15 23:14:09 +08:00
2016-01-28 05:21:36 +08:00
"github.com/influxdata/telegraf"
2021-01-28 23:54:29 +08:00
"github.com/influxdata/telegraf/internal"
2016-01-21 02:57:35 +08:00
"github.com/influxdata/telegraf/plugins/inputs"
2015-09-03 07:16:52 +08:00
)
2022-05-24 21:49:47 +08:00
//go:embed sample.conf
var sampleConfig string
2021-05-21 03:21:20 +08:00
const (
defaultPingDataBytesSize = 56
)
2015-09-03 07:16:52 +08:00
type Ping struct {
2024-12-18 01:10:18 +08:00
Urls [ ] string ` toml:"urls" ` // URLs to ping
Method string ` toml:"method" ` // Method defines how to ping (native or exec)
Count int ` toml:"count" ` // Number of pings to send (ping -c <COUNT>)
PingInterval float64 ` toml:"ping_interval" ` // Interval at which to ping (ping -i <INTERVAL>)
Timeout float64 ` toml:"timeout" ` // Per-ping timeout, in seconds. 0 means no timeout (ping -W <TIMEOUT>)
Deadline int ` toml:"deadline" ` // Ping deadline, in seconds. 0 means no deadline. (ping -w <DEADLINE>)
Interface string ` toml:"interface" ` // Interface or source address to send ping from (ping -I/-S <INTERFACE/SRC_ADDR>)
Percentiles [ ] int ` toml:"percentiles" ` // Calculate the given percentiles when using native method
Binary string ` toml:"binary" ` // Ping executable binary
// Arguments for ping command. When arguments are not empty, system binary will be used and other options (ping_interval, timeout, etc.) will be ignored
Arguments [ ] string ` toml:"arguments" `
IPv4 bool ` toml:"ipv4" ` // Whether to resolve addresses using ipv4 or not.
IPv6 bool ` toml:"ipv6" ` // Whether to resolve addresses using ipv6 or not.
Size * int ` toml:"size" ` // Packet size
Log telegraf . Logger ` toml:"-" `
wg sync . WaitGroup // wg is used to wait for ping with multiple URLs
calcInterval time . Duration // Pre-calculated interval and timeout
calcTimeout time . Duration
sourceAddress string
pingHost hostPingerFunc // host ping function
nativePingFunc nativePingFunc
}
2019-07-12 06:07:58 +08:00
2024-12-18 01:10:18 +08:00
// hostPingerFunc 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 hostPingerFunc func ( binary string , timeout float64 , args ... string ) ( string , error )
2018-10-02 08:38:13 +08:00
2024-12-18 01:10:18 +08:00
type nativePingFunc func ( destination string ) ( * pingStats , error )
2018-10-02 08:38:13 +08:00
2024-12-18 01:10:18 +08:00
type durationSlice [ ] time . Duration
2024-03-20 20:02:23 +08:00
2024-12-18 01:10:18 +08:00
type pingStats struct {
ping . Statistics
ttl int
}
2019-07-12 06:07:58 +08:00
2024-12-18 01:10:18 +08:00
func ( * Ping ) SampleConfig ( ) string {
return sampleConfig
}
2019-07-12 06:07:58 +08:00
2024-12-18 01:10:18 +08:00
func ( p * Ping ) Init ( ) error {
if p . Count < 1 {
return errors . New ( "bad number of packets to transmit" )
}
2020-12-10 01:54:51 +08:00
2024-12-18 01:10:18 +08:00
// The interval cannot be below 0.2 seconds, matching ping implementation: https://linux.die.net/man/8/ping
if p . PingInterval < 0.2 {
p . calcInterval = time . Duration ( .2 * float64 ( time . Second ) )
} else {
p . calcInterval = time . Duration ( p . PingInterval * float64 ( time . Second ) )
}
2021-05-21 03:21:20 +08:00
2024-12-18 01:10:18 +08:00
// If no timeout is given default to 5 seconds, matching original implementation
if p . Timeout == 0 {
p . calcTimeout = time . Duration ( 5 ) * time . Second
} else {
p . calcTimeout = time . Duration ( p . Timeout ) * time . Second
}
2015-09-03 07:16:52 +08:00
2024-12-18 01:10:18 +08:00
return nil
2022-05-24 21:49:47 +08:00
}
2016-01-28 05:21:36 +08:00
func ( p * Ping ) Gather ( acc telegraf . Accumulator ) error {
2019-12-04 03:27:33 +08:00
for _ , host := range p . Urls {
p . wg . Add ( 1 )
go func ( host string ) {
defer p . wg . Done ( )
switch p . Method {
case "native" :
p . pingToURLNative ( host , acc )
default :
p . pingToURL ( host , acc )
}
} ( host )
2018-10-02 08:38:13 +08:00
}
2017-10-27 04:35:37 +08:00
2018-10-02 08:38:13 +08:00
p . wg . Wait ( )
2017-10-27 04:35:37 +08:00
2018-10-02 08:38:13 +08:00
return nil
}
2017-08-17 02:59:41 +08:00
2021-01-27 06:02:43 +08:00
func ( p * Ping ) nativePing ( destination string ) ( * pingStats , error ) {
ps := & pingStats { }
2015-09-03 07:16:52 +08:00
2021-01-27 06:02:43 +08:00
pinger , err := ping . NewPinger ( destination )
2016-02-02 07:36:08 +08:00
if err != nil {
2021-02-09 00:18:40 +08:00
return nil , fmt . Errorf ( "failed to create new pinger: %w" , err )
2016-02-02 07:36:08 +08:00
}
2015-09-03 07:16:52 +08:00
2021-03-31 02:08:54 +08:00
pinger . SetPrivileged ( true )
2020-03-25 08:02:23 +08:00
2024-03-20 20:02:23 +08:00
if p . IPv4 && p . IPv6 {
pinger . SetNetwork ( "ip" )
} else if p . IPv4 {
pinger . SetNetwork ( "ip4" )
} else if p . IPv6 {
2021-01-27 06:02:43 +08:00
pinger . SetNetwork ( "ip6" )
}
2018-10-02 08:38:13 +08:00
2021-05-21 03:21:20 +08:00
if p . Method == "native" {
pinger . Size = defaultPingDataBytesSize
if p . Size != nil {
pinger . Size = * p . Size
}
}
2024-07-10 21:50:44 +08:00
// Support either an IP address or interface name
if p . Interface != "" && p . sourceAddress == "" {
if addr := net . ParseIP ( p . Interface ) ; addr != nil {
p . sourceAddress = p . Interface
} else {
i , err := net . InterfaceByName ( p . Interface )
if err != nil {
return nil , fmt . Errorf ( "failed to get interface: %w" , err )
}
addrs , err := i . Addrs ( )
if err != nil {
return nil , fmt . Errorf ( "failed to get the address of interface: %w" , err )
}
if len ( addrs ) == 0 {
return nil , fmt . Errorf ( "no address found for interface %s" , p . Interface )
}
p . sourceAddress = addrs [ 0 ] . ( * net . IPNet ) . IP . String ( )
}
}
2021-01-29 01:30:00 +08:00
pinger . Source = p . sourceAddress
2021-01-27 06:02:43 +08:00
pinger . Interval = p . calcInterval
if p . Deadline > 0 {
2021-02-10 00:50:57 +08:00
pinger . Timeout = time . Duration ( p . Deadline ) * time . Second
2020-03-25 08:02:23 +08:00
}
2021-01-27 06:02:43 +08:00
// Get Time to live (TTL) of first response, matching original implementation
once := & sync . Once { }
pinger . OnRecv = func ( pkt * ping . Packet ) {
once . Do ( func ( ) {
2022-09-27 03:04:30 +08:00
ps . ttl = pkt . TTL
2021-01-27 06:02:43 +08:00
} )
2020-03-25 08:02:23 +08:00
}
2021-01-27 06:02:43 +08:00
pinger . Count = p . Count
err = pinger . Run ( )
if err != nil {
2021-03-31 02:08:54 +08:00
if strings . Contains ( err . Error ( ) , "operation not permitted" ) {
if runtime . GOOS == "linux" {
2024-02-09 01:32:30 +08:00
return nil , errors . New ( "permission changes required, enable CAP_NET_RAW capabilities (refer to the ping plugin's README.md for more info)" )
2021-03-31 02:08:54 +08:00
}
2024-02-09 01:32:30 +08:00
return nil , errors . New ( "permission changes required, refer to the ping plugin's README.md for more info" )
2021-03-31 02:08:54 +08:00
}
2022-09-27 03:04:30 +08:00
return nil , err
2015-09-03 07:16:52 +08:00
}
2020-03-25 08:02:23 +08:00
2021-01-27 06:02:43 +08:00
ps . Statistics = * pinger . Statistics ( )
2020-03-25 08:02:23 +08:00
2021-01-27 06:02:43 +08:00
return ps , nil
2020-03-25 08:02:23 +08:00
}
2019-07-12 06:07:58 +08:00
2020-03-25 08:02:23 +08:00
func ( p * Ping ) pingToURLNative ( destination string , acc telegraf . Accumulator ) {
2021-01-27 06:02:43 +08:00
tags := map [ string ] string { "url" : destination }
2019-07-12 06:07:58 +08:00
2021-01-27 06:02:43 +08:00
stats , err := p . nativePingFunc ( destination )
2020-03-25 08:02:23 +08:00
if err != nil {
2021-03-11 01:11:19 +08:00
p . Log . Errorf ( "ping failed: %s" , err . Error ( ) )
2024-10-25 18:54:05 +08:00
fields := make ( map [ string ] interface { } , 1 )
2021-01-27 06:02:43 +08:00
if strings . Contains ( err . Error ( ) , "unknown" ) {
fields [ "result_code" ] = 1
} else {
fields [ "result_code" ] = 2
}
acc . AddFields ( "ping" , fields , tags )
2020-03-25 08:02:23 +08:00
return
}
2024-10-25 18:54:05 +08:00
fields := map [ string ] interface { } {
2021-01-27 06:02:43 +08:00
"result_code" : 0 ,
"packets_transmitted" : stats . PacketsSent ,
"packets_received" : stats . PacketsRecv ,
2019-10-23 07:46:57 +08:00
}
2021-01-27 06:02:43 +08:00
if stats . PacketsSent == 0 {
2021-03-11 01:11:19 +08:00
p . Log . Debug ( "no packets sent" )
2021-01-27 06:02:43 +08:00
fields [ "result_code" ] = 2
acc . AddFields ( "ping" , fields , tags )
return
2015-09-03 07:16:52 +08:00
}
2019-07-12 06:07:58 +08:00
2021-01-27 06:02:43 +08:00
if stats . PacketsRecv == 0 {
2021-03-11 01:11:19 +08:00
p . Log . Debug ( "no packets received" )
2021-01-27 06:02:43 +08:00
fields [ "result_code" ] = 1
fields [ "percent_packet_loss" ] = float64 ( 100 )
acc . AddFields ( "ping" , fields , tags )
return
}
2019-07-12 06:07:58 +08:00
2021-02-10 00:50:57 +08:00
sort . Sort ( durationSlice ( stats . Rtts ) )
2021-01-27 06:02:43 +08:00
for _ , perc := range p . Percentiles {
2021-11-15 23:14:09 +08:00
var value = percentile ( stats . Rtts , perc )
2021-01-27 06:02:43 +08:00
var field = fmt . Sprintf ( "percentile%v_ms" , perc )
fields [ field ] = float64 ( value . Nanoseconds ( ) ) / float64 ( time . Millisecond )
}
2019-10-23 07:46:57 +08:00
2021-01-27 06:02:43 +08:00
// 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" ] = stats . ttl
2019-10-23 07:46:57 +08:00
}
2021-01-27 06:02:43 +08:00
fields [ "percent_packet_loss" ] = float64 ( stats . PacketLoss )
fields [ "minimum_response_ms" ] = float64 ( stats . MinRtt ) / float64 ( time . Millisecond )
fields [ "average_response_ms" ] = float64 ( stats . AvgRtt ) / float64 ( time . Millisecond )
fields [ "maximum_response_ms" ] = float64 ( stats . MaxRtt ) / float64 ( time . Millisecond )
fields [ "standard_deviation_ms" ] = float64 ( stats . StdDevRtt ) / float64 ( time . Millisecond )
2019-07-12 06:07:58 +08:00
acc . AddFields ( "ping" , fields , tags )
2015-09-03 07:16:52 +08:00
}
2024-12-18 01:10:18 +08:00
func ( p durationSlice ) Len ( ) int { return len ( p ) }
2020-12-10 01:54:51 +08:00
func ( p durationSlice ) Less ( i , j int ) bool { return p [ i ] < p [ j ] }
2024-12-18 01:10:18 +08:00
func ( p durationSlice ) Swap ( i , j int ) { p [ i ] , p [ j ] = p [ j ] , p [ i ] }
2020-12-10 01:54:51 +08:00
// R7 from Hyndman and Fan (1996), which matches Excel
func percentile ( values durationSlice , perc int ) time . Duration {
2021-01-27 06:02:43 +08:00
if len ( values ) == 0 {
return 0
}
2020-12-10 01:54:51 +08:00
if perc < 0 {
perc = 0
}
if perc > 100 {
perc = 100
}
var percFloat = float64 ( perc ) / 100.0
var count = len ( values )
var rank = percFloat * float64 ( count - 1 )
var rankInteger = int ( rank )
var rankFraction = rank - math . Floor ( rank )
if rankInteger >= count - 1 {
return values [ count - 1 ]
}
2021-02-09 00:18:40 +08:00
upper := values [ rankInteger + 1 ]
lower := values [ rankInteger ]
return lower + time . Duration ( rankFraction * float64 ( upper - lower ) )
2020-12-10 01:54:51 +08:00
}
2021-01-28 23:54:29 +08:00
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 + 5 ) )
return string ( out ) , err
}
2015-09-03 07:16:52 +08:00
func init ( ) {
2016-01-28 05:21:36 +08:00
inputs . Add ( "ping" , func ( ) telegraf . Input {
2021-01-27 06:02:43 +08:00
p := & Ping {
2021-01-28 23:54:29 +08:00
pingHost : hostPinger ,
2016-10-12 19:12:07 +08:00
PingInterval : 1.0 ,
Count : 1 ,
Timeout : 1.0 ,
2018-02-16 12:11:07 +08:00
Deadline : 10 ,
2019-07-12 06:07:58 +08:00
Method : "exec" ,
2018-10-02 08:38:13 +08:00
Binary : "ping" ,
2024-10-24 17:03:31 +08:00
Arguments : make ( [ ] string , 0 ) ,
Percentiles : make ( [ ] int , 0 ) ,
2016-10-12 19:12:07 +08:00
}
2021-01-27 06:02:43 +08:00
p . nativePingFunc = p . nativePing
return p
2015-09-03 07:16:52 +08:00
} )
}