init
This commit is contained in:
commit
d9ce3912ed
|
@ -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
|
|
@ -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)))
|
||||
}
|
Loading…
Reference in New Issue