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}}
Net Downtime
Counts rel abs

{{.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))) }