From d9ce3912ed3b5eb235715fa3283bf5e35407d805 Mon Sep 17 00:00:00 2001 From: lemmi Date: Tue, 21 Jan 2020 22:44:28 +0100 Subject: [PATCH] init --- .gitignore | 26 +++++ main.go | 285 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 311 insertions(+) create mode 100644 .gitignore create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06ee5b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +rmon +# Created by https://www.gitignore.io/api/go +# Edit at https://www.gitignore.io/?templates=go + +### Go ### +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +### Go Patch ### +/vendor/ +/Godeps/ + +# End of https://www.gitignore.io/api/go diff --git a/main.go b/main.go new file mode 100644 index 0000000..0682670 --- /dev/null +++ b/main.go @@ -0,0 +1,285 @@ +package main + +import ( + "bytes" + "fmt" + "html/template" + "io" + "log" + "net" + "net/http" + "os" + "sort" + "sync" + "time" + + "golang.org/x/sys/unix" + + "github.com/vishvananda/netlink" +) + +var ( + startTime = time.Now() +) + +type route struct { + Dst *net.IPNet + Counter uint + UnreachableDuration time.Duration + UnreachableSince *time.Time +} + +func (r route) DurationUntilRounded(t time.Time) time.Duration { + return r.DurationUntil(t).Round(time.Second) +} + +func (r route) DurationUntil(t time.Time) time.Duration { + ret := r.UnreachableDuration + if r.UnreachableSince != nil { + ret += t.Sub(*r.UnreachableSince) + } + return ret +} + +func (r route) Downtime(now time.Time) float64 { + return float64(r.DurationUntil(now)) / float64(now.Sub(startTime)) * 100 +} + +func (r route) Noise(now time.Time) bool { + return r.Counter <= uint(now.Sub(startTime).Hours()) && r.Downtime(now) < 0.01 +} + +func (r route) String() string { + now := time.Now() + return fmt.Sprintf("%44s %5d %2.3f%% %s", r.Dst, r.Counter, r.Downtime(now), r.DurationUntil(now)) +} + +type routeStats struct { + sync.Mutex + stats map[string]*route +} + +func newRouteStats() *routeStats { + return &routeStats{ + stats: make(map[string]*route), + } +} + +func (rs *routeStats) update(ru netlink.RouteUpdate) { + rs.Lock() + defer rs.Unlock() + + switch ru.Type { + case unix.RTM_NEWROUTE: + rs.add(ru) + case unix.RTM_DELROUTE: + rs.del(ru) + default: + fmt.Fprintf(os.Stderr, "Unknown route type %d\n", ru.Type) + } +} + +func (rs *routeStats) add(ru netlink.RouteUpdate) { + key := ru.Route.Dst.String() + r := rs.stats[key] + if r == nil { + r = &route{Dst: ru.Route.Dst} + } + r.Counter++ + if r.UnreachableSince == nil { + t := time.Now() + r.UnreachableSince = &t + } + rs.stats[key] = r +} + +func (rs *routeStats) del(ru netlink.RouteUpdate) { + key := ru.Route.Dst.String() + r := rs.stats[key] + if r == nil { + r = &route{Dst: ru.Route.Dst} + return + } + r.UnreachableDuration += time.Since(*r.UnreachableSince) + r.UnreachableSince = nil + rs.stats[key] = r +} + +func (rs *routeStats) getAll() []route { + ret := make([]route, 0, len(rs.stats)) + + rs.Lock() + defer rs.Unlock() + for _, v := range rs.stats { + ret = append(ret, *v) + } + + return ret +} + +func (rs *routeStats) getLongest() []route { + ret := rs.getAll() + + t := time.Now() + sort.Slice(ret, func(i, j int) bool { + return ret[i].DurationUntil(t) > ret[j].DurationUntil(t) + }) + + return ret +} + +func monitor(done <-chan struct{}, rs *routeStats) { + rups := make(chan netlink.RouteUpdate, 64) + opts := netlink.RouteSubscribeOptions{ + ErrorCallback: func(err error) { + fmt.Fprintf(os.Stderr, "%s\n", err, err) + }, + } + if err := netlink.RouteSubscribeWithOptions(rups, done, opts); err != nil { + log.Fatal(err) + } + + for ru := range rups { + if ru.Route.Type != unix.RTN_UNREACHABLE { + continue + } + // show unly babel routes + if ru.Route.Protocol != 42 { + continue + } + rs.update(ru) + } +} + +func render(w io.Writer, rs []route) error { + const tmplHTML = ` + + + + + FFF Wall-of-not-so-reachable-networks + + + +

FFF Wall-of-not-so-reachable-networks

+ +

+ + + +
Start:{{ .Start.Format "2006-01-02 15:04:05" }}
Update:{{ .Now.Format "2006-01-02 15:04:05" }}
+

+ + Show All + + + + + + + + + + + + + + + + + {{- $now := .Now -}} + {{- range .R }} + + + + + + + {{- end}} + +
NetDowntime
Countsrelabs

{{.Dst}}{{.Counter}}{{.Downtime $now | printf "%.3f%%"}}{{.DurationUntilRounded $now}}
+ + +` + + data := struct { + Now time.Time + Start time.Time + R []route + }{ + time.Now(), + startTime, + rs, + } + t := template.Must(template.New("main").Parse(tmplHTML)) + return t.Execute(w, data) +} + +func cachedRender(rs *routeStats) http.HandlerFunc { + var m sync.Mutex + var timestamp time.Time + var content []byte + return func(w http.ResponseWriter, r *http.Request) { + m.Lock() + if time.Since(timestamp) > 5*time.Second { + var buf bytes.Buffer + if err := render(&buf, rs.getLongest()); err != nil { + fmt.Fprintln(os.Stderr, err) + } + content = buf.Bytes() + timestamp = time.Now() + } + m.Unlock() + + w.Header().Add("Cache-Control", "max-age=5") + if _, err := w.Write(content); err != nil { + fmt.Fprintln(os.Stderr, err) + } + } +} + +func main() { + rs := newRouteStats() + done := make(chan struct{}) + go monitor(done, rs) + log.Fatal(http.ListenAndServe(":8080", cachedRender(rs))) +}