This commit is contained in:
lemmi 2020-01-21 22:44:28 +01:00
commit d9ce3912ed
2 changed files with 311 additions and 0 deletions

26
.gitignore vendored Normal file
View File

@ -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

285
main.go Normal file
View File

@ -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 = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FFF Wall-of-not-so-reachable-networks</title>
<style>
html {
margin: 0 auto;
padding: 0;
font-size: 11pt;
}
body {
font-family: sans-serif;
text-align: center;
}
table {
border-spacing: 0;
margin: auto;
}
td {
text-align: right;
padding: 0.1em 2em;
font-family: monospace;
}
.dst {
text-align: left;
}
.unreachable {
color: darkred;
background-color: lightgray;
}
table tr.noise {
display: none;
}
tbody tr:hover {
background-color: silver;
}
#showAll:checked ~ table tr.noise {
color: midnightblue;
display: table-row;
}
</style>
</head>
<body>
<h1>FFF Wall-of-not-so-reachable-networks</h1>
<p>
<table>
<tr><td> Start:</td><td>{{ .Start.Format "2006-01-02 15:04:05" }}</td></tr>
<tr><td>Update:</td><td>{{ .Now.Format "2006-01-02 15:04:05" }}</td></tr>
</table>
</p>
Show All <input id="showAll" type="checkbox">
<table>
<thead>
<tr>
<th>Net</th>
<th colspan="3">Downtime</th>
</tr>
<tr>
<th></th>
<th>Counts</th>
<th>rel</th>
<th>abs</th>
</tr>
</thead>
<tbody>
<tr><td colspan="4"><hr/></td></tr>
{{- $now := .Now -}}
{{- range .R }}
<tr class="
{{- if .UnreachableSince }}unreachable{{end -}}
{{- if .Noise $now }} noise{{end -}}
">
<td class="dst">{{.Dst}}</td>
<td class="counter">{{.Counter}}</td>
<td class="downtime">{{.Downtime $now | printf "%.3f%%"}}</td>
<td class="duration">{{.DurationUntilRounded $now}}</td>
</tr>
{{- end}}
</tbody>
</table>
</body>
</html>
`
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)))
}