Add light traffic theme, interactive map and metric detail dialogs
This commit is contained in:
18
README.md
18
README.md
@@ -631,3 +631,21 @@ Compose и пересоздайте контейнер, если логиров
|
||||
[NPM+](https://github.com/ZoeyVid/NPMplus/blob/develop/README.md), а не Dashboard.
|
||||
Stream-логи NPM+ не считаются HTTP-запросами; сетевые потоки собираются conntrack.
|
||||
Как и раньше, первый запуск чтения начинается с конца существующих логов.
|
||||
|
||||
## Интерактивная карта (v0.24.0)
|
||||
|
||||
Карта трафика использует ту же настройку `proxmox-theme`, что и основной
|
||||
Dashboard. Кнопка солнца/луны меняет тему; выбор сохраняется в браузере.
|
||||
Светлая тема распространяется на карту, график, карточки и модальные окна.
|
||||
|
||||
Масштаб карты — от 100% до 600%: кнопки +/−, колесо мыши или жест двумя
|
||||
пальцами. Увеличенную карту можно перетаскивать; ↺ возвращает весь мир.
|
||||
При фокусе на карте доступны клавиши +/−, 0 и стрелки. Наведение подсвечивает
|
||||
контур страны и показывает суточные счётчики, а нажатие открывает подробности.
|
||||
|
||||
Четыре карточки статистики открывают окна с итогом, объяснением метрики,
|
||||
разбивкой по странам и примерами событий. Счётчики стран считаются сервером
|
||||
по всей доступной истории за 24 часа с текущими фильтрами. Примеры — до 30
|
||||
совпадений из последних 100 событий, что явно отмечено в интерфейсе. Открытое
|
||||
окно фиксирует данные на момент открытия; временная отметка показана внутри.
|
||||
Сетевые потоки остаются отдельным текущим снимком, не суточной статистикой.
|
||||
|
||||
File diff suppressed because one or more lines are too long
28
traffic.go
28
traffic.go
@@ -51,7 +51,17 @@ type TrafficCount struct {
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
type TrafficCountryDetail struct {
|
||||
Country string `json:"country"`
|
||||
Requests int64 `json:"requests"`
|
||||
UniqueIPs int64 `json:"uniqueIps"`
|
||||
Alerts int64 `json:"alerts"`
|
||||
Decisions int64 `json:"decisions"`
|
||||
LastSeen int64 `json:"lastSeen"`
|
||||
}
|
||||
type TrafficView struct {
|
||||
CountryDetails []TrafficCountryDetail `json:"countryDetails"`
|
||||
|
||||
Network *NetworkMapView `json:"network,omitempty"`
|
||||
Events []TrafficEvent `json:"events"`
|
||||
Sources []TrafficSource `json:"sources"`
|
||||
@@ -222,6 +232,24 @@ func (s *Store) TrafficView(domain, kind string, external bool, hideMonitoring .
|
||||
return v, e
|
||||
}
|
||||
}
|
||||
detailRows, detailErr := s.db.Query(`SELECT country,SUM(kind='request'),COUNT(DISTINCT CASE WHEN kind='request' THEN ip END),SUM(kind='alert'),SUM(kind='decision'),MAX(ts) FROM traffic_events`+where+` GROUP BY country`, args...)
|
||||
if detailErr != nil {
|
||||
return v, detailErr
|
||||
}
|
||||
v.CountryDetails = []TrafficCountryDetail{}
|
||||
for detailRows.Next() {
|
||||
var d TrafficCountryDetail
|
||||
if err = detailRows.Scan(&d.Country, &d.Requests, &d.UniqueIPs, &d.Alerts, &d.Decisions, &d.LastSeen); err != nil {
|
||||
detailRows.Close()
|
||||
return v, err
|
||||
}
|
||||
v.CountryDetails = append(v.CountryDetails, d)
|
||||
}
|
||||
detailErr = detailRows.Err()
|
||||
detailRows.Close()
|
||||
if detailErr != nil {
|
||||
return v, detailErr
|
||||
}
|
||||
r, err := s.db.Query(`SELECT CAST((ts/60)*60 AS TEXT),COUNT(*) FROM traffic_events`+where+` AND kind='request' AND ts>=? GROUP BY ts/60 ORDER BY ts/60`, append(args, now-3600)...)
|
||||
if err != nil {
|
||||
return v, err
|
||||
|
||||
44
traffic_country_test.go
Normal file
44
traffic_country_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTrafficCountryDetailsUseHistoryAndFilters(t *testing.T) {
|
||||
s, a := trafficFixture(t)
|
||||
if _, err := s.SaveTrafficSettings(TrafficSettings{Enabled: true, AgentID: a.ID, MonitoringRules: []MonitoringRule{{IP: "8.8.8.8"}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
events := []TrafficEvent{}
|
||||
for i := 0; i < 120; i++ {
|
||||
events = append(events, TrafficEvent{ID: fmt.Sprint("http-", i), Time: time.Now().Unix(), Kind: "request", IP: "8.8.8.8", Domain: "example.test", Path: "/", Status: 200})
|
||||
}
|
||||
events = append(events, TrafficEvent{ID: "alert", Time: time.Now().Unix(), Kind: "alert", IP: "8.8.8.8", Domain: "example.test", Reason: "test"})
|
||||
if err := s.SaveTraffic(a.ID, TrafficBatch{Events: events}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v, err := s.TrafficView("", "", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(v.Events) != 100 || len(v.CountryDetails) != 1 {
|
||||
t.Fatalf("events=%d countries=%+v", len(v.Events), v.CountryDetails)
|
||||
}
|
||||
d := v.CountryDetails[0]
|
||||
if d.Requests != 120 || d.UniqueIPs != 1 || d.Alerts != 1 || d.LastSeen == 0 {
|
||||
t.Fatalf("%+v", d)
|
||||
}
|
||||
v, err = s.TrafficView("example.test", "", false, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(v.CountryDetails) != 1 || v.CountryDetails[0].Requests != 0 || v.CountryDetails[0].Alerts != 1 {
|
||||
t.Fatalf("filter: %+v", v.CountryDetails)
|
||||
}
|
||||
v, err = s.TrafficView("other.test", "", false)
|
||||
if err != nil || len(v.CountryDetails) != 0 {
|
||||
t.Fatalf("domain: %+v %v", v.CountryDetails, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user