1
0
Fork 0
telegraf/plugins/inputs/ping/ping.go
Daniel Baumann 4978089aab
Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-24 07:26:29 +02:00

313 lines
8.6 KiB
Go

//go:generate ../../../tools/readme_config_includer/generator
package ping
import (
_ "embed"
"errors"
"fmt"
"math"
"net"
"os/exec"
"runtime"
"sort"
"strings"
"sync"
"time"
ping "github.com/prometheus-community/pro-bing"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/inputs"
)
//go:embed sample.conf
var sampleConfig string
const (
defaultPingDataBytesSize = 56
)
type Ping struct {
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
}
// 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)
type nativePingFunc func(destination string) (*pingStats, error)
type durationSlice []time.Duration
type pingStats struct {
ping.Statistics
ttl int
}
func (*Ping) SampleConfig() string {
return sampleConfig
}
func (p *Ping) Init() error {
if p.Count < 1 {
return errors.New("bad number of packets to transmit")
}
// 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))
}
// 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
}
return nil
}
func (p *Ping) Gather(acc telegraf.Accumulator) error {
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)
}
p.wg.Wait()
return nil
}
func (p *Ping) nativePing(destination string) (*pingStats, error) {
ps := &pingStats{}
pinger, err := ping.NewPinger(destination)
if err != nil {
return nil, fmt.Errorf("failed to create new pinger: %w", err)
}
pinger.SetPrivileged(true)
if p.IPv4 && p.IPv6 {
pinger.SetNetwork("ip")
} else if p.IPv4 {
pinger.SetNetwork("ip4")
} else if p.IPv6 {
pinger.SetNetwork("ip6")
}
if p.Method == "native" {
pinger.Size = defaultPingDataBytesSize
if p.Size != nil {
pinger.Size = *p.Size
}
}
// 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()
}
}
pinger.Source = p.sourceAddress
pinger.Interval = p.calcInterval
if p.Deadline > 0 {
pinger.Timeout = time.Duration(p.Deadline) * time.Second
}
// Get Time to live (TTL) of first response, matching original implementation
once := &sync.Once{}
pinger.OnRecv = func(pkt *ping.Packet) {
once.Do(func() {
ps.ttl = pkt.TTL
})
}
pinger.Count = p.Count
err = pinger.Run()
if err != nil {
if strings.Contains(err.Error(), "operation not permitted") {
if runtime.GOOS == "linux" {
return nil, errors.New("permission changes required, enable CAP_NET_RAW capabilities (refer to the ping plugin's README.md for more info)")
}
return nil, errors.New("permission changes required, refer to the ping plugin's README.md for more info")
}
return nil, err
}
ps.Statistics = *pinger.Statistics()
return ps, nil
}
func (p *Ping) pingToURLNative(destination string, acc telegraf.Accumulator) {
tags := map[string]string{"url": destination}
stats, err := p.nativePingFunc(destination)
if err != nil {
p.Log.Errorf("ping failed: %s", err.Error())
fields := make(map[string]interface{}, 1)
if strings.Contains(err.Error(), "unknown") {
fields["result_code"] = 1
} else {
fields["result_code"] = 2
}
acc.AddFields("ping", fields, tags)
return
}
fields := map[string]interface{}{
"result_code": 0,
"packets_transmitted": stats.PacketsSent,
"packets_received": stats.PacketsRecv,
}
if stats.PacketsSent == 0 {
p.Log.Debug("no packets sent")
fields["result_code"] = 2
acc.AddFields("ping", fields, tags)
return
}
if stats.PacketsRecv == 0 {
p.Log.Debug("no packets received")
fields["result_code"] = 1
fields["percent_packet_loss"] = float64(100)
acc.AddFields("ping", fields, tags)
return
}
sort.Sort(durationSlice(stats.Rtts))
for _, perc := range p.Percentiles {
var value = percentile(stats.Rtts, perc)
var field = fmt.Sprintf("percentile%v_ms", perc)
fields[field] = float64(value.Nanoseconds()) / float64(time.Millisecond)
}
// 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
}
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)
acc.AddFields("ping", fields, tags)
}
func (p durationSlice) Len() int { return len(p) }
func (p durationSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p durationSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
// R7 from Hyndman and Fan (1996), which matches Excel
func percentile(values durationSlice, perc int) time.Duration {
if len(values) == 0 {
return 0
}
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]
}
upper := values[rankInteger+1]
lower := values[rankInteger]
return lower + time.Duration(rankFraction*float64(upper-lower))
}
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
}
func init() {
inputs.Add("ping", func() telegraf.Input {
p := &Ping{
pingHost: hostPinger,
PingInterval: 1.0,
Count: 1,
Timeout: 1.0,
Deadline: 10,
Method: "exec",
Binary: "ping",
Arguments: make([]string, 0),
Percentiles: make([]int, 0),
}
p.nativePingFunc = p.nativePing
return p
})
}