358 lines
9.3 KiB
Go
358 lines
9.3 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/nxadm/tail"
|
|
ping "github.com/prometheus-community/pro-bing"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
)
|
|
|
|
type IperfResult struct {
|
|
Start struct {
|
|
Version string `json:"version"`
|
|
SystemInfo string `json:"system_info"`
|
|
ConnectTime float64 `json:"connect_time"`
|
|
} `json:"start"`
|
|
End struct {
|
|
SumSent struct {
|
|
Seconds float64 `json:"seconds"`
|
|
Bytes int64 `json:"bytes"`
|
|
BitsPerSec float64 `json:"bits_per_second"`
|
|
Retransmits int `json:"retransmits"`
|
|
} `json:"sum_sent"`
|
|
SumReceived struct {
|
|
Seconds float64 `json:"seconds"`
|
|
Bytes int64 `json:"bytes"`
|
|
BitsPerSec float64 `json:"bits_per_second"`
|
|
} `json:"sum_received"`
|
|
CPUUtilization struct {
|
|
HostTotal float64 `json:"host_total"`
|
|
RemoteTotal float64 `json:"remote_total"`
|
|
} `json:"cpu_utilization_percent"`
|
|
} `json:"end"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
var Error *log.Logger
|
|
var Warning *log.Logger
|
|
|
|
var (
|
|
pingRttHistogram = prometheus.NewHistogramVec(
|
|
prometheus.HistogramOpts{
|
|
Name: "ping_rtt_seconds",
|
|
Help: "Round-trip time histogram in seconds",
|
|
Buckets: []float64{.001, .002, .005, .01, .02, .05, .1, .2, .5, 1},
|
|
},
|
|
[]string{"target", "name"},
|
|
)
|
|
|
|
iperfBandwidthGauge = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: "iperf_bandwidth_bits_per_second",
|
|
Help: "Bandwidth measured by iperf3 in bits per second",
|
|
},
|
|
[]string{"direction", "server"},
|
|
)
|
|
|
|
httpRttHistogram = prometheus.NewHistogramVec(
|
|
prometheus.HistogramOpts{
|
|
Name: "http_rtt_seconds",
|
|
Help: "HTTP request round-trip time histogram in seconds",
|
|
Buckets: []float64{.001, .002, .005, .01, .02, .05, .1, .2, .5, 1},
|
|
},
|
|
[]string{"target", "name"},
|
|
)
|
|
)
|
|
|
|
var iperfServer = "138.199.199.16"
|
|
var iperfUploadPort = 3332
|
|
var iperfDownloadPort = 3333
|
|
|
|
func init() {
|
|
log.Printf("Initializing application...")
|
|
log.SetFlags(log.Lmicroseconds | log.Lshortfile)
|
|
Error = log.New(io.MultiWriter(os.Stderr, os.Stdout),
|
|
fmt.Sprintf("%sERROR:%s ", "\033[0;101m", "\033[0m"),
|
|
log.Lmicroseconds|log.Lshortfile)
|
|
Warning = log.New(io.MultiWriter(os.Stdout),
|
|
fmt.Sprintf("%sWarning:%s ", "\033[0;93m", "\033[0m"),
|
|
log.Lmicroseconds|log.Lshortfile)
|
|
|
|
// Register only the essential metrics
|
|
prometheus.MustRegister(pingRttHistogram)
|
|
|
|
// Register new iperf3 metrics
|
|
prometheus.MustRegister(iperfBandwidthGauge)
|
|
|
|
// Register HTTP metrics
|
|
prometheus.MustRegister(httpRttHistogram)
|
|
|
|
log.Printf("Registered all Prometheus metrics")
|
|
}
|
|
|
|
type PingTarget struct {
|
|
IP string
|
|
Name string
|
|
}
|
|
|
|
func updateMetrics(stats *ping.Statistics, name string) {
|
|
target := stats.Addr
|
|
|
|
if stats.PacketsRecv > 0 {
|
|
histogram := pingRttHistogram.WithLabelValues(target, name)
|
|
histogram.Observe(stats.AvgRtt.Seconds())
|
|
//log.Printf("Ping metrics for %s (%s): RTT=%v, PacketsRecv=%d/%d",
|
|
//name, target, stats.AvgRtt, stats.PacketsRecv, stats.PacketsSent)
|
|
} else {
|
|
log.Printf("No packets received for %s (%s)", name, target)
|
|
}
|
|
}
|
|
|
|
// IperfTest represents a runnable iperf3 test configuration
|
|
type IperfTest struct {
|
|
ServerHost string
|
|
UploadPort int
|
|
DownloadPort int
|
|
}
|
|
|
|
// NewIperfTest creates a new IperfTest with default values
|
|
func NewIperfTest(serverHost string, uploadPort, downloadPort int) *IperfTest {
|
|
return &IperfTest{
|
|
ServerHost: serverHost,
|
|
UploadPort: uploadPort,
|
|
DownloadPort: downloadPort,
|
|
}
|
|
}
|
|
|
|
var bitrateRegex = regexp.MustCompile(`(\d+) Mbits/sec`)
|
|
|
|
// runTest executes an iperf3 test with the given direction
|
|
func (t *IperfTest) runTest(reverse bool) error {
|
|
direction := "upload"
|
|
port := t.UploadPort
|
|
if reverse {
|
|
direction = "download"
|
|
port = t.DownloadPort
|
|
}
|
|
|
|
log.Printf("Starting iperf3 %s test to %s:%d", direction, t.ServerHost, port)
|
|
|
|
// Check if iperf3 is installed
|
|
if _, err := exec.LookPath("iperf3"); err != nil {
|
|
return fmt.Errorf("iperf3 not found: %v", err)
|
|
}
|
|
|
|
title := "upload"
|
|
if reverse {
|
|
title = "download"
|
|
}
|
|
|
|
logFile := fmt.Sprintf("iperf3-%s-%s.log", direction, t.ServerHost)
|
|
_, err := os.Stat(logFile)
|
|
if err == nil {
|
|
log.Printf("Removing existing log file %s", logFile)
|
|
os.Remove(logFile)
|
|
}
|
|
log.Printf("Logging iperf3 %s test to %s", direction, logFile)
|
|
|
|
args := []string{
|
|
"-c", t.ServerHost,
|
|
"-p", fmt.Sprintf("%d", port),
|
|
"-Z",
|
|
"-T", title,
|
|
"-t", "0",
|
|
"-b", "600M", // Limit bandwidth to 200 Mbits
|
|
"-f", "m",
|
|
"-i", "1",
|
|
"--logfile", logFile,
|
|
}
|
|
|
|
if reverse {
|
|
args = append(args, "-R")
|
|
}
|
|
|
|
log.Printf("Running iperf3 %s test with args: %v", direction, args)
|
|
|
|
cmd := exec.Command("iperf3", args...)
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to start iperf3: %v", err)
|
|
}
|
|
|
|
itail, err := tail.TailFile(
|
|
logFile, tail.Config{Follow: true, ReOpen: true})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to tail iperf3 log file: %v", err)
|
|
}
|
|
|
|
for line := range itail.Lines {
|
|
bitrate := bitrateRegex.FindStringSubmatch(line.Text)
|
|
if len(bitrate) > 1 {
|
|
mbps, err := strconv.ParseFloat(bitrate[1], 64)
|
|
if err != nil {
|
|
log.Printf("failed to parse bitrate: %v", err)
|
|
}
|
|
|
|
log.Printf("iperf3 %s bandwidth: %.2f Mbps", direction, mbps)
|
|
if reverse {
|
|
iperfBandwidthGauge.WithLabelValues(direction, t.ServerHost).Set(mbps)
|
|
} else {
|
|
iperfBandwidthGauge.WithLabelValues(direction, t.ServerHost).Set(mbps)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := cmd.Wait(); err != nil {
|
|
return fmt.Errorf("iperf3 %s test failed: %v",
|
|
direction, err)
|
|
}
|
|
log.Printf("iperf3 %s test completed successfully", direction)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Start begins continuous iperf3 testing in both directions
|
|
func (t *IperfTest) Start() {
|
|
log.Printf("Starting continuous iperf3 tests to %s", t.ServerHost)
|
|
|
|
// Start upload test goroutine
|
|
go func() {
|
|
for {
|
|
if err := t.runTest(false); err != nil {
|
|
Error.Printf("Upload test failed: %v", err)
|
|
}
|
|
time.Sleep(1 * time.Second)
|
|
log.Printf("Restarting upload test immediately")
|
|
}
|
|
}()
|
|
|
|
// Start download test goroutine
|
|
go func() {
|
|
for {
|
|
if err := t.runTest(true); err != nil {
|
|
Error.Printf("Download test failed: %v", err)
|
|
}
|
|
time.Sleep(1 * time.Second)
|
|
log.Printf("Restarting download test immediately")
|
|
}
|
|
}()
|
|
}
|
|
|
|
type HttpTarget struct {
|
|
URL string
|
|
Name string
|
|
}
|
|
|
|
func probeHTTP(target HttpTarget) {
|
|
log.Printf("Starting HTTP probe for %s (%s)", target.Name, target.URL)
|
|
parsedURL, err := url.Parse(target.URL)
|
|
if err != nil {
|
|
Error.Printf("Invalid URL %s: %v", target.URL, err)
|
|
return
|
|
}
|
|
|
|
client := &http.Client{
|
|
Timeout: 20 * time.Second,
|
|
}
|
|
|
|
for {
|
|
start := time.Now()
|
|
log.Printf("Sending HTTP request to %s", target.URL)
|
|
resp, err := client.Get(target.URL)
|
|
duration := time.Since(start)
|
|
|
|
if err != nil {
|
|
Error.Printf("HTTP probe to %s failed: %v (took %v)", target.URL, err, duration)
|
|
} else {
|
|
httpRttHistogram.WithLabelValues(parsedURL.Host, target.Name).Observe(duration.Seconds())
|
|
log.Printf("HTTP probe to %s succeeded in %v", target.URL, duration)
|
|
resp.Body.Close()
|
|
}
|
|
|
|
log.Printf("Sleeping for 10 seconds before next HTTP probe to %s", target.URL)
|
|
time.Sleep(10 * time.Second)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
log.Printf("Starting network monitoring application")
|
|
|
|
targets := []PingTarget{
|
|
{IP: "8.8.8.8", Name: "google-dns-1"},
|
|
{IP: "1.1.1.1", Name: "cloudflare-dns-1"},
|
|
{IP: "8.8.4.4", Name: "google-dns-2"},
|
|
{IP: "1.0.0.1", Name: "cloudflare-dns-2"},
|
|
{IP: "208.67.222.222", Name: "opendns-1"},
|
|
{IP: "208.67.220.220", Name: "opendns-2"},
|
|
{IP: "192.168.1.254", Name: "router"},
|
|
}
|
|
log.Printf("Configured ping targets: %v", targets)
|
|
|
|
// Add HTTP targets
|
|
httpTargets := []HttpTarget{
|
|
{URL: "http://192.168.1.254", Name: "router-http"},
|
|
}
|
|
log.Printf("Configured HTTP targets: %v", httpTargets)
|
|
|
|
// Start HTTP probes
|
|
for _, target := range httpTargets {
|
|
go probeHTTP(target)
|
|
}
|
|
|
|
//Start Prometheus HTTP server
|
|
go func() {
|
|
http.Handle("/metrics", promhttp.Handler())
|
|
if err := http.ListenAndServe(":2112", nil); err != nil {
|
|
Error.Printf("starting prometheus listener failed: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
}()
|
|
|
|
pingers := make([]*ping.Pinger, len(targets))
|
|
for i, target := range targets {
|
|
pinger, err := ping.NewPinger(target.IP)
|
|
if err != nil {
|
|
Error.Printf("new pinger for %s failed: %v", target.IP, err)
|
|
return
|
|
}
|
|
//pinger.SetPrivileged(true)
|
|
pinger.RecordRtts = false
|
|
pinger.RecordTTLs = false
|
|
pingers[i] = pinger
|
|
go func(t PingTarget) {
|
|
log.Printf("Starting pinger for %s (%s)", t.Name, t.IP)
|
|
err = pinger.Run()
|
|
if err != nil {
|
|
Error.Printf("pinger for %s (%s) failed: %v", t.Name, t.IP, err)
|
|
return
|
|
}
|
|
}(target)
|
|
}
|
|
|
|
// Initialize and start iperf3 testing with separate ports
|
|
iperfTest := NewIperfTest(iperfServer, iperfUploadPort, iperfDownloadPort)
|
|
iperfTest.Start()
|
|
|
|
log.Printf("All monitoring services started, entering main loop")
|
|
for {
|
|
time.Sleep(1 * time.Second)
|
|
for i, pinger := range pingers {
|
|
if pinger != nil {
|
|
stats := pinger.Statistics()
|
|
updateMetrics(stats, targets[i].Name)
|
|
}
|
|
}
|
|
}
|
|
}
|