fix(api): SNMP/hosts — одна строка на IP (DISTINCT ON), лимиты для больших сканов

Made-with: Cursor
This commit is contained in:
Луценко Андрей Анатольевич
2026-04-10 15:35:45 +10:00
parent 009aeb5dbf
commit a36edbc26b
4 changed files with 69 additions and 36 deletions
+6 -6
View File
@@ -181,11 +181,11 @@ func (h *Handler) getScanHosts(w http.ResponseWriter, r *http.Request) {
return return
} }
limit := 500 limit := 200000
if raw := r.URL.Query().Get("limit"); raw != "" { if raw := r.URL.Query().Get("limit"); raw != "" {
n, err := strconv.Atoi(raw) n, err := strconv.Atoi(raw)
if err != nil || n <= 0 || n > 5000 { if err != nil || n <= 0 || n > 500000 {
writeError(w, http.StatusBadRequest, "limit must be between 1 and 5000") writeError(w, http.StatusBadRequest, "limit must be between 1 and 500000")
return return
} }
limit = n limit = n
@@ -233,11 +233,11 @@ func (h *Handler) getScanSNMP(w http.ResponseWriter, r *http.Request) {
return return
} }
limit := 1000 limit := 200000
if raw := r.URL.Query().Get("limit"); raw != "" { if raw := r.URL.Query().Get("limit"); raw != "" {
n, err := strconv.Atoi(raw) n, err := strconv.Atoi(raw)
if err != nil || n <= 0 || n > 10000 { if err != nil || n <= 0 || n > 500000 {
writeError(w, http.StatusBadRequest, "limit must be between 1 and 10000") writeError(w, http.StatusBadRequest, "limit must be between 1 and 500000")
return return
} }
limit = n limit = n
+15 -6
View File
@@ -291,12 +291,17 @@ values ($1, $2, $3, $4)`
func (s *PostgresStore) ListHostResults(scanID string, limit int) []HostResult { func (s *PostgresStore) ListHostResults(scanID string, limit int) []HostResult {
if limit <= 0 { if limit <= 0 {
limit = 500 limit = 200000
} }
// Одна актуальная строка на IP (иначе при limit по времени часть хостов без SNMP в UI).
query := ` query := `
select ip::text, is_up, checked_at select ip::text, is_up, checked_at
from scan_hosts from (
where scan_id = $1 select distinct on (ip) ip, is_up, checked_at
from scan_hosts
where scan_id = $1
order by ip asc, checked_at desc
) t
order by checked_at desc order by checked_at desc
limit $2` limit $2`
rows, err := s.db.QueryContext(context.Background(), query, scanID, limit) rows, err := s.db.QueryContext(context.Background(), query, scanID, limit)
@@ -380,13 +385,17 @@ values ($1, $2, $3, $4, $5, $6, $7, $8)`
func (s *PostgresStore) ListSNMPResults(scanID string, limit int) []SNMPResult { func (s *PostgresStore) ListSNMPResults(scanID string, limit int) []SNMPResult {
if limit <= 0 { if limit <= 0 {
limit = 1000 limit = 200000
} }
query := ` query := `
select ip::text, success, coalesce(sys_name, ''), coalesce(sys_descr, ''), coalesce(sys_object_id, ''), select ip::text, success, coalesce(sys_name, ''), coalesce(sys_descr, ''), coalesce(sys_object_id, ''),
coalesce(error_text, ''), checked_at coalesce(error_text, ''), checked_at
from scan_snmp from (
where scan_id = $1 select distinct on (ip) ip, success, sys_name, sys_descr, sys_object_id, error_text, checked_at
from scan_snmp
where scan_id = $1
order by ip asc, checked_at desc
) t
order by checked_at desc order by checked_at desc
limit $2` limit $2`
rows, err := s.db.QueryContext(context.Background(), query, scanID, limit) rows, err := s.db.QueryContext(context.Background(), query, scanID, limit)
+46 -22
View File
@@ -221,21 +221,11 @@ func (s *MemoryStore) SaveHostResult(scanID string, host HostResult) error {
} }
func (s *MemoryStore) ListHostResults(scanID string, limit int) []HostResult { func (s *MemoryStore) ListHostResults(scanID string, limit int) []HostResult {
if limit <= 0 {
limit = 500
}
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
items := s.hosts[scanID] items := s.hosts[scanID]
if len(items) <= limit { return dedupeHostsLatest(items, limit)
out := make([]HostResult, len(items))
copy(out, items)
return out
}
out := make([]HostResult, limit)
copy(out, items[:limit])
return out
} }
func (s *MemoryStore) SaveOpenPortResult(scanID string, result OpenPortResult) error { func (s *MemoryStore) SaveOpenPortResult(scanID string, result OpenPortResult) error {
@@ -279,21 +269,11 @@ func (s *MemoryStore) SaveSNMPResult(scanID string, result SNMPResult) error {
} }
func (s *MemoryStore) ListSNMPResults(scanID string, limit int) []SNMPResult { func (s *MemoryStore) ListSNMPResults(scanID string, limit int) []SNMPResult {
if limit <= 0 {
limit = 1000
}
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
items := s.snmp[scanID] items := s.snmp[scanID]
if len(items) <= limit { return dedupeSNMPLatest(items, limit)
out := make([]SNMPResult, len(items))
copy(out, items)
return out
}
out := make([]SNMPResult, limit)
copy(out, items[:limit])
return out
} }
func (s *MemoryStore) SaveLLDPResult(scanID string, result LLDPResult) error { func (s *MemoryStore) SaveLLDPResult(scanID string, result LLDPResult) error {
@@ -367,3 +347,47 @@ func newID() (string, error) {
} }
return hex.EncodeToString(b[:]), nil return hex.EncodeToString(b[:]), nil
} }
func dedupeHostsLatest(items []HostResult, limit int) []HostResult {
if limit <= 0 {
limit = 200000
}
m := make(map[string]HostResult, len(items))
for _, h := range items {
cur, ok := m[h.IP]
if !ok || h.CheckedAt.After(cur.CheckedAt) {
m[h.IP] = h
}
}
out := make([]HostResult, 0, len(m))
for _, h := range m {
out = append(out, h)
}
sort.Slice(out, func(i, j int) bool { return out[i].CheckedAt.After(out[j].CheckedAt) })
if len(out) > limit {
out = out[:limit]
}
return out
}
func dedupeSNMPLatest(items []SNMPResult, limit int) []SNMPResult {
if limit <= 0 {
limit = 200000
}
m := make(map[string]SNMPResult, len(items))
for _, r := range items {
cur, ok := m[r.IP]
if !ok || r.CheckedAt.After(cur.CheckedAt) {
m[r.IP] = r
}
}
out := make([]SNMPResult, 0, len(m))
for _, r := range m {
out = append(out, r)
}
sort.Slice(out, func(i, j int) bool { return out[i].CheckedAt.After(out[j].CheckedAt) })
if len(out) > limit {
out = out[:limit]
}
return out
}
+2 -2
View File
@@ -418,8 +418,8 @@
deviceTableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;color:#64748b;">Загрузка…</td></tr>'; deviceTableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;color:#64748b;">Загрузка…</td></tr>';
try { try {
const [hRes, sRes, lRes] = await Promise.all([ const [hRes, sRes, lRes] = await Promise.all([
fetch(`/api/scans/${encodeURIComponent(scanId)}/hosts?limit=5000`), fetch(`/api/scans/${encodeURIComponent(scanId)}/hosts?limit=200000`),
fetch(`/api/scans/${encodeURIComponent(scanId)}/snmp?limit=5000`), fetch(`/api/scans/${encodeURIComponent(scanId)}/snmp?limit=200000`),
fetch(`/api/scans/${encodeURIComponent(scanId)}/lldp?limit=10000`), fetch(`/api/scans/${encodeURIComponent(scanId)}/lldp?limit=10000`),
]); ]);
if (!hRes.ok) throw new Error(await hRes.text()); if (!hRes.ok) throw new Error(await hRes.text());