package main import ( "net/http" "strconv" "strings" "time" ) type TrafficIPDetail struct { IP string `json:"ip"` Country string `json:"country"` Requests int64 `json:"requests"` LastSeen int64 `json:"lastSeen"` City string `json:"city,omitempty"` Region string `json:"region,omitempty"` } type TrafficHistory struct { Events []TrafficEvent `json:"events"` IPs []TrafficIPDetail `json:"ips"` Total int64 `json:"total"` Page int `json:"page"` PageSize int `json:"pageSize"` Snapshot int64 `json:"snapshot"` MaxSequence int64 `json:"maxSequence"` CityReady bool `json:"cityReady"` } func (s *Store) trafficHistoryRoute(mux *http.ServeMux) { mux.HandleFunc("/api/traffic/history", func(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { w.WriteHeader(405) return } if !s.TrafficSettings().Enabled { http.Error(w, "Карта отключена", 404) return } q := r.URL.Query() page, _ := strconv.Atoi(q.Get("page")) snapshot, _ := strconv.ParseInt(q.Get("snapshot"), 10, 64) maxSeq, _ := strconv.ParseInt(q.Get("maxSequence"), 10, 64) if q.Get("maxSequence") == "0" { maxSeq = -1 } if page < 0 || page > 10000 { http.Error(w, "Invalid page", 400) return } kind := q.Get("kind") metricType := q.Get("type") if metricType != "" && metricType != "request" && metricType != "alert" && metricType != "decision" { http.Error(w, "Invalid type", 400) return } if kind != "" && kind != "request" && kind != "alert" && kind != "decision" { http.Error(w, "Invalid kind", 400) return } result, err := s.TrafficHistory(q.Get("domain"), kind, q.Get("country"), q.Get("ip"), q.Get("type"), q.Get("external") == "1", q.Get("hideMonitoring") == "1", q.Get("group") == "ip", page, snapshot, maxSeq) writeJSON(w, result, err) }) } func (s *Store) TrafficHistory(domain, kind, country, ip, metricType string, external, hide, group bool, page int, snapshot, maxSeq int64) (TrafficHistory, error) { now := time.Now().Unix() if snapshot <= 0 || snapshot > now { snapshot = now } if snapshot < now-86400 { snapshot = now } settings := s.TrafficSettings() v := TrafficHistory{Events: []TrafficEvent{}, IPs: []TrafficIPDetail{}, Page: page, PageSize: 50, Snapshot: snapshot, CityReady: trafficCityReady()} if maxSeq == 0 { if err := s.db.QueryRow(`SELECT COALESCE(MAX(seq),0) FROM traffic_events WHERE agent=?`, settings.AgentID).Scan(&maxSeq); err != nil { return v, err } } v.MaxSequence = maxSeq where := ` WHERE agent=? AND ts>=? AND ts<=? AND seq<=?` args := []any{settings.AgentID, snapshot - 86400, snapshot, maxSeq} if hide { clause, values := monitoringSQL(settings.MonitoringRules) where += clause args = append(args, values...) } if external { where += ` AND country!='LAN'` } for _, filter := range []struct{ column, value string }{{"domain", domain}, {"kind", kind}, {"kind", metricType}, {"country", country}, {"ip", ip}} { if filter.value != "" { where += " AND " + filter.column + "=?" args = append(args, filter.value) } } count := "COUNT(*)" if group { where += ` AND kind='request'` count = "COUNT(DISTINCT ip)" } if err := s.db.QueryRow("SELECT "+count+" FROM traffic_events"+where, args...).Scan(&v.Total); err != nil { return v, err } paged := append(append([]any{}, args...), v.PageSize, page*v.PageSize) if group { rows, err := s.db.Query(`SELECT ip,MIN(country),COUNT(*),MAX(ts) FROM traffic_events`+where+` GROUP BY ip ORDER BY COUNT(*) DESC,ip LIMIT ? OFFSET ?`, paged...) if err != nil { return v, err } defer rows.Close() for rows.Next() { var item TrafficIPDetail if err = rows.Scan(&item.IP, &item.Country, &item.Requests, &item.LastSeen); err != nil { return v, err } item.City, item.Region = lookupTrafficCity(item.IP) v.IPs = append(v.IPs, item) } return v, rows.Err() } rows, err := s.db.Query(`SELECT seq,event_id,ts,kind,ip,domain,method,path,status,reason,country,agent FROM traffic_events`+where+` ORDER BY ts DESC,seq DESC LIMIT ? OFFSET ?`, paged...) if err != nil { return v, err } defer rows.Close() for rows.Next() { var e TrafficEvent if err = rows.Scan(&e.Sequence, &e.ID, &e.Time, &e.Kind, &e.IP, &e.Domain, &e.Method, &e.Path, &e.Status, &e.Reason, &e.Country, &e.Agent); err != nil { return v, err } e.Monitoring = isMonitoringRequest(e, settings.MonitoringRules) e.City, e.Region = lookupTrafficCity(e.IP) v.Events = append(v.Events, e) } return v, rows.Err() } func cityName(names map[string]string) string { for _, lang := range []string{"ru", "en"} { if s := strings.TrimSpace(names[lang]); s != "" { return s } } return "" }