Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions internal/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func (s *Server) overview(w http.ResponseWriter, r *http.Request) {
out["now_bps"] = float64(nt.Wire) * 8 / 300
}

se, err := s.Store.SeriesByApp(q, 6)
se, err := s.Store.SeriesByApp(q, 8)
if err != nil {
fail(w, err)
return
Expand Down Expand Up @@ -238,14 +238,22 @@ func (s *Server) sankey(w http.ResponseWriter, r *http.Request) {
fail(w, err)
return
}
byHost := r.URL.Query().Get("by") != "segment"
links, err := s.Store.Sankey(q, byHost, 10)
mode := r.URL.Query().Get("by")
if mode == "" {
mode = "host"
}
// 8 hosts, clients or servers (the colours there are); 6 applications,
// services and countries
n0, n2 := 8, 6
if mode == "conv" {
n2 = 8
}
links, err := s.Store.Sankey(q, mode, n0, n2)
if err != nil {
fail(w, err)
return
}
// keep the largest nodes per column (10 hosts or 6 segments, 6
// applications, 6 countries), fold the rest into "__other__"
// fold everything past the largest nodes of each column into "__other__"
top := func(get func(store.SankeyLink) string, n ...int) map[string]bool {
t := map[string]uint64{}
for _, l := range links {
Expand All @@ -268,13 +276,10 @@ func (s *Server) sankey(w http.ResponseWriter, r *http.Request) {
}
return m
}
first := 6
if byHost {
first = 10
}
first := n0
ks := top(func(l store.SankeyLink) string { return l.Seg }, first)
ka := top(func(l store.SankeyLink) string { return l.App })
kc := top(func(l store.SankeyLink) string { return l.CC })
kc := top(func(l store.SankeyLink) string { return l.CC }, n2)
fold := func(m map[string]bool, k string) string {
if m[k] {
return k
Expand All @@ -299,11 +304,7 @@ func (s *Server) sankey(w http.ResponseWriter, r *http.Request) {
sort.Slice(out, func(i, j int) bool { return out[i].V > out[j].V })
return out
}
by := "segment"
if byHost {
by = "host"
}
writeJSON(w, http.StatusOK, map[string]any{"by": by, "seg_app": flat(l1), "app_cc": flat(l2)})
writeJSON(w, http.StatusOK, map[string]any{"by": mode, "seg_app": flat(l1), "app_cc": flat(l2)})
}

func (s *Server) records(w http.ResponseWriter, r *http.Request) {
Expand Down
39 changes: 24 additions & 15 deletions internal/store/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,25 +637,34 @@ type SankeyLink struct {
Wire uint64
}

// Sankey returns traffic by segment (or, with byHost, by internal host),
// application and remote country. By host, only the top hosts are kept
// apart; the rest are summed as "__other__".
func (s *Store) Sankey(q Query, byHost bool, hosts int) ([]SankeyLink, error) {
// Sankey modes: the three columns of the flow paths chart.
var sankeyCols = map[string][3]string{
// the internal side of each conversation (the client, or the server of
// inbound traffic) → application → remote country
"host": {"CASE WHEN dir = 2 THEN server ELSE client END", "app", sankeyCC},
"segment": {"segment", "app", sankeyCC},
"conv": {"client", portProtoExpr, "server"},
}

const sankeyCC = `CASE WHEN dir = 3 THEN '__internal__' WHEN peer_cc = '' THEN '__unknown__' ELSE peer_cc END`

// Sankey returns traffic by the mode's three columns. The first and last
// columns keep their top n0 and n2 values; the rest are "__other__".
func (s *Store) Sankey(q Query, mode string, n0, n2 int) ([]SankeyLink, error) {
where, args, err := q.where()
if err != nil {
return nil, err
}
cc := `CASE WHEN dir = 3 THEN '__internal__' WHEN peer_cc = '' THEN '__unknown__' ELSE peer_cc END`
sqlq := fmt.Sprintf(`SELECT segment, app, %s, sum(wire) FROM %s WHERE %s GROUP BY 1,2,3`, cc, s.Source(q.From, q.To), where)
if byHost {
// the internal side of each conversation: the client, or the server
// of inbound traffic
sqlq = fmt.Sprintf(`WITH b AS (SELECT CASE WHEN dir = %d THEN server ELSE client END AS h, app, %s AS cc, sum(wire) AS w
FROM %s WHERE %s GROUP BY 1,2,3),
t AS (SELECT h FROM b GROUP BY h ORDER BY sum(w) DESC LIMIT %d)
SELECT CASE WHEN h IN (SELECT h FROM t) THEN h ELSE '__other__' END, app, cc, sum(w) FROM b GROUP BY 1,2,3`,
DirInbound, cc, s.Source(q.From, q.To), where, hosts)
}
c, ok := sankeyCols[mode]
if !ok {
return nil, fmt.Errorf("unknown flow paths mode %q", mode)
}
sqlq := fmt.Sprintf(`WITH b AS (SELECT %s AS c0, %s AS c1, %s AS c2, sum(wire) AS w FROM %s WHERE %s GROUP BY 1,2,3),
t0 AS (SELECT c0 FROM b GROUP BY c0 ORDER BY sum(w) DESC LIMIT %d),
t2 AS (SELECT c2 FROM b GROUP BY c2 ORDER BY sum(w) DESC LIMIT %d)
SELECT CASE WHEN c0 IN (SELECT c0 FROM t0) THEN c0 ELSE '__other__' END,
c1, CASE WHEN c2 IN (SELECT c2 FROM t2) THEN c2 ELSE '__other__' END, sum(w)
FROM b GROUP BY 1,2,3`, c[0], c[1], c[2], s.Source(q.From, q.To), where, n0, n2)
rows, err := s.DB.Query(sqlq, args...)
if err != nil {
return nil, err
Expand Down
10 changes: 9 additions & 1 deletion internal/web/static/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
--bg:#f4f6f9; --surface:#ffffff; --surface-2:#f3f5f8; --line:#e4e8ee; --line-2:#cfd6e0;
--ink:#1b2432; --ink-2:#4c5869; --ink-3:#6f7a8a;
--accent:#2a78d6; --accent-soft:#e8f1fc; --accent-ink:#1c5cab;
--c1:#2a78d6; --c2:#eb6834; --c3:#1baf7a; --c4:#eda100; --c5:#e87ba4; --c6:#008300; --other:#b9bfc9;
--c1:#2a78d6; --c2:#eb6834; --c3:#1baf7a; --c4:#eda100; --c5:#e87ba4; --c6:#008300; --c7:#4a3aa7; --c8:#e34948; --other:#b9bfc9;
--base:#8e98a6;
--good:#0ca30c; --warn:#fab219; --crit:#d03b3b; --warn-ink:#8a5d00;
}
Expand Down Expand Up @@ -57,6 +57,14 @@ nav.dash button[aria-current="page"]{background:var(--accent-soft);color:var(--a
.primary{border:0;background:var(--accent);color:#fff;border-radius:7px;padding:8px 14px;cursor:pointer;font-weight:600}
.row2{display:flex;gap:8px;align-items:center;padding:0 20px 10px;min-height:34px;flex-wrap:wrap}
.row2 .hint{color:var(--ink-3);font-size:13px}
/* filter dropdowns */
.fbar{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;padding:0 20px 8px}
.fbar[hidden]{display:none}
.fbar label{display:grid;gap:2px;font-size:11.5px;color:var(--ink-3);font-weight:600}
.fbar input{border:1px solid var(--line-2);border-radius:7px;padding:5px 9px;background:var(--surface);font-size:13px;color:var(--ink);min-width:0}
.fbar input.on{border-color:var(--accent);background:var(--accent-soft)}
.fbar input::placeholder{color:var(--ink-3);font-weight:400}
@media (max-width:820px){.fbar{grid-template-columns:repeat(2,minmax(0,1fr));padding-inline:12px}}
.chip{display:inline-flex;align-items:center;gap:4px;background:var(--accent-soft);color:var(--accent-ink);border-radius:14px;padding:2px 4px 2px 11px;font-size:13px}
[dir=rtl] .chip{padding:2px 11px 2px 4px}
.chip.neg{background:#fdecec;color:var(--crit)}
Expand Down
Loading
Loading