214 lines
5.1 KiB
Go
214 lines
5.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"github.com/montanaflynn/stats"
|
|
"github.com/prometheus-community/pro-bing"
|
|
"math"
|
|
"os"
|
|
"os/signal"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
type ContinuousPinger struct {
|
|
Pings []*probing.Packet
|
|
Lock *sync.RWMutex
|
|
}
|
|
|
|
var (
|
|
Waybar = flag.Bool("waybar", false, "output waybar json format")
|
|
PingCount = flag.Int("count", 5, "how many pings to average")
|
|
PingWarningLimit = flag.Int("warn", 50, "ping warning limit")
|
|
PingCritLimit = flag.Int("crit", 100, "ping critical limit")
|
|
PacketLossWarnLimit = flag.Int("pwarn", 10, "packetloss warning limit")
|
|
PacketLossCritLimit = flag.Int("pcrit", 25, "packetloss critical limit")
|
|
Host = flag.String("host", "google.com", "host to ping")
|
|
WarnColor = flag.String("warnc", "ffbf00", "hex color for warning ping (without #)")
|
|
CritColor = flag.String("critc", "ff5050", "hex color for critical ping (without #)")
|
|
)
|
|
|
|
type PingStats struct {
|
|
AvgRtt time.Duration
|
|
StdDev time.Duration
|
|
PacketLoss float64
|
|
}
|
|
|
|
//goland:noinspection ALL
|
|
const (
|
|
ResetColor = "%{F-}"
|
|
)
|
|
|
|
type WaybarOut struct {
|
|
Class string `json:"class"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
func formatLinePolybar(stats *PingStats) {
|
|
if stats.PacketLoss >= 100.0 {
|
|
// fontawesome/forkawesome don't have a fitting (free) icon, so we use 🚫 from UTF-8 symbols/emoji
|
|
fmt.Printf("%%{F#%s}🚫\n", *CritColor)
|
|
return
|
|
}
|
|
|
|
rttColor := ResetColor
|
|
packetColor := ResetColor
|
|
|
|
switch {
|
|
case int(stats.AvgRtt.Milliseconds()) >= *PingWarningLimit:
|
|
rttColor = fmt.Sprintf("%%{F#%s}", *WarnColor)
|
|
case int(stats.AvgRtt.Milliseconds()) >= *PingCritLimit:
|
|
rttColor = fmt.Sprintf("%%{F#%s}", *CritColor)
|
|
}
|
|
|
|
switch {
|
|
case int(math.Round(stats.PacketLoss)) >= *PacketLossWarnLimit:
|
|
packetColor = fmt.Sprintf("%%{F#%s}", *WarnColor)
|
|
case int(math.Round(stats.PacketLoss)) >= *PacketLossCritLimit:
|
|
packetColor = fmt.Sprintf("%%{F#%s}", *CritColor)
|
|
}
|
|
|
|
fmt.Printf("%s\uE4E2 %d \uE43C %dms %s\uF1B2 %d%%\n", rttColor, stats.AvgRtt.Milliseconds(),
|
|
stats.StdDev.Milliseconds(), packetColor, int(math.Round(stats.PacketLoss)))
|
|
}
|
|
|
|
func formatLineWaybar(stats *PingStats) {
|
|
res := new(WaybarOut)
|
|
|
|
res.Text = fmt.Sprintf("\uE4E2 %d \uE43C %dms \uF1B2 %d%%", stats.AvgRtt.Milliseconds(),
|
|
stats.StdDev.Milliseconds(), int(math.Round(stats.PacketLoss)))
|
|
res.Class = "good"
|
|
|
|
switch {
|
|
case int(math.Round(stats.PacketLoss)) >= *PacketLossWarnLimit || int(stats.AvgRtt.Milliseconds()) >= *PingWarningLimit:
|
|
res.Class = "warning"
|
|
fallthrough
|
|
case int(math.Round(stats.PacketLoss)) >= *PacketLossCritLimit || int(stats.AvgRtt.Milliseconds()) >= *PingCritLimit:
|
|
res.Class = "critical"
|
|
}
|
|
|
|
jOut, err := json.Marshal(res)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
fmt.Println(string(jOut))
|
|
}
|
|
|
|
func (cp *ContinuousPinger) Push(packet *probing.Packet) {
|
|
cp.Lock.Lock()
|
|
defer cp.Lock.Unlock()
|
|
if len(cp.Pings) >= *PingCount {
|
|
cp.Pings = cp.Pings[:len(cp.Pings)-1]
|
|
}
|
|
cp.Pings = append([]*probing.Packet{packet}, cp.Pings...)
|
|
}
|
|
|
|
func (cp *ContinuousPinger) Stats(dSent, dReceive int) (*PingStats, error) {
|
|
cp.Lock.RLock()
|
|
defer cp.Lock.RUnlock()
|
|
var rttArray []float64
|
|
ps := new(PingStats)
|
|
|
|
var sum int64
|
|
var pkgsInArr int
|
|
for _, pkg := range cp.Pings {
|
|
if pkg != nil {
|
|
rttArray = append(rttArray, float64(pkg.Rtt))
|
|
sum += int64(pkg.Rtt)
|
|
pkgsInArr++
|
|
}
|
|
}
|
|
|
|
if pkgsInArr == 0 {
|
|
return nil, fmt.Errorf("no packets received")
|
|
}
|
|
|
|
ps.AvgRtt = time.Duration(sum / int64(pkgsInArr))
|
|
ps.PacketLoss = math.Min(100, math.Max(100-(float64(dReceive)/float64(dSent)*100), 0))
|
|
if math.IsNaN(ps.PacketLoss) || math.IsInf(ps.PacketLoss, 0) {
|
|
ps.PacketLoss = 100
|
|
}
|
|
|
|
stdDev, err := stats.StandardDeviation(rttArray)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ps.StdDev = time.Duration(stdDev)
|
|
|
|
return ps, nil
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
killSignals := make(chan os.Signal, 1)
|
|
signal.Notify(killSignals, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
lp := new(ContinuousPinger)
|
|
lp.Pings = make([]*probing.Packet, *PingCount)
|
|
lp.Lock = new(sync.RWMutex)
|
|
ts := 0
|
|
tr := 0
|
|
lr := time.Now().UTC()
|
|
|
|
var pinger *probing.Pinger
|
|
var err error
|
|
for pinger, err = probing.NewPinger(*Host); err != nil; {
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
|
|
pinger.RecordRtts = false
|
|
|
|
pinger.OnRecv = func(pkt *probing.Packet) {
|
|
lp.Push(pkt)
|
|
pStats, err := lp.Stats(pinger.PacketsSent-ts, pinger.PacketsRecv-tr)
|
|
lr = time.Now().UTC()
|
|
ts = pinger.PacketsSent
|
|
tr = pinger.PacketsRecv
|
|
switch {
|
|
case err != nil:
|
|
fmt.Println(err)
|
|
case *Waybar:
|
|
formatLineWaybar(pStats)
|
|
default:
|
|
formatLinePolybar(pStats)
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
for {
|
|
if time.Since(lr).Milliseconds() > 1500 {
|
|
pStats, err := lp.Stats(pinger.PacketsSent-ts, pinger.PacketsRecv-tr)
|
|
ts = pinger.PacketsSent
|
|
tr = pinger.PacketsRecv
|
|
switch {
|
|
case err != nil:
|
|
fmt.Println(err)
|
|
case *Waybar:
|
|
formatLineWaybar(pStats)
|
|
default:
|
|
formatLinePolybar(pStats)
|
|
}
|
|
}
|
|
time.Sleep(time.Second)
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
for {
|
|
if err = pinger.Run(); err != nil {
|
|
fmt.Println(err)
|
|
time.Sleep(time.Second)
|
|
}
|
|
}
|
|
}()
|
|
|
|
for range killSignals {
|
|
break
|
|
}
|
|
pinger.Stop()
|
|
}
|