bleve_stats_service_geo.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. package nginx_log
  2. import (
  3. "context"
  4. "fmt"
  5. "sort"
  6. "strings"
  7. "github.com/0xJacky/Nginx-UI/internal/geolite"
  8. "github.com/blevesearch/bleve/v2"
  9. "github.com/blevesearch/bleve/v2/search"
  10. "github.com/blevesearch/bleve/v2/search/query"
  11. )
  12. // GeoStats represents geographic statistics
  13. type GeoStats struct {
  14. RegionCode string `json:"region_code"`
  15. Country string `json:"country"`
  16. Province string `json:"province,omitempty"`
  17. City string `json:"city,omitempty"`
  18. Count int `json:"count"`
  19. Percent float64 `json:"percent"`
  20. }
  21. // WorldMapData represents data for world map visualization
  22. type WorldMapData struct {
  23. RegionCode string `json:"code"`
  24. ISP string `json:"isp,omitempty"`
  25. Value int `json:"value"`
  26. Percent float64 `json:"percent"`
  27. }
  28. // ChinaMapData represents data for China map visualization
  29. type ChinaMapData struct {
  30. Name string `json:"name"` // Province name for ECharts map
  31. Value int `json:"value"`
  32. Percent float64 `json:"percent"`
  33. Cities []CityData `json:"cities,omitempty"`
  34. }
  35. // CityData represents city-level data
  36. type CityData struct {
  37. Name string `json:"name"`
  38. Value int `json:"value"`
  39. Percent float64 `json:"percent"`
  40. }
  41. // normalizeRegionForWorldMap unifies HK, MO, TW under CN for world map display
  42. func normalizeRegionForWorldMap(regionCode string) string {
  43. if regionCode == "HK" || regionCode == "MO" || regionCode == "TW" {
  44. return "CN"
  45. }
  46. return regionCode
  47. }
  48. // normalizeProvinceName standardizes Chinese province names
  49. func normalizeProvinceName(province string) string {
  50. if province == "" {
  51. return province
  52. }
  53. // Common province name mappings - ensure they end with proper suffixes
  54. provinceMap := map[string]string{
  55. // Provinces (省)
  56. "北京": "北京市",
  57. "天津": "天津市",
  58. "上海": "上海市",
  59. "重庆": "重庆市",
  60. "河北": "河北省",
  61. "山西": "山西省",
  62. "辽宁": "辽宁省",
  63. "吉林": "吉林省",
  64. "黑龙江": "黑龙江省",
  65. "江苏": "江苏省",
  66. "浙江": "浙江省",
  67. "安徽": "安徽省",
  68. "福建": "福建省",
  69. "江西": "江西省",
  70. "山东": "山东省",
  71. "河南": "河南省",
  72. "湖北": "湖北省",
  73. "湖南": "湖南省",
  74. "广东": "广东省",
  75. "海南": "海南省",
  76. "四川": "四川省",
  77. "贵州": "贵州省",
  78. "云南": "云南省",
  79. "陕西": "陕西省",
  80. "甘肃": "甘肃省",
  81. "青海": "青海省",
  82. "台湾": "台湾省",
  83. // Autonomous regions (自治区)
  84. "内蒙古": "内蒙古自治区",
  85. "广西": "广西壮族自治区",
  86. "西藏": "西藏自治区",
  87. "宁夏": "宁夏回族自治区",
  88. "新疆": "新疆维吾尔自治区",
  89. }
  90. // Check if we have a mapping for this province
  91. if normalized, exists := provinceMap[province]; exists {
  92. return normalized
  93. }
  94. // If already has proper suffix, return as-is
  95. if strings.HasSuffix(province, "省") || strings.HasSuffix(province, "市") ||
  96. strings.HasSuffix(province, "自治区") || strings.HasSuffix(province, "特别行政区") {
  97. return province
  98. }
  99. // Default: assume it's a province and add "省" suffix
  100. return province + "省"
  101. }
  102. // getProvinceShortName returns the short name for Chinese provinces for map display
  103. func getProvinceShortName(fullName string) string {
  104. // Map of full province names to short names for map display
  105. shortNameMap := map[string]string{
  106. // Municipalities (直辖市) - keep as is
  107. "北京市": "北京",
  108. "天津市": "天津",
  109. "上海市": "上海",
  110. "重庆市": "重庆",
  111. // Provinces (省) - remove suffix
  112. "河北省": "河北",
  113. "山西省": "山西",
  114. "辽宁省": "辽宁",
  115. "吉林省": "吉林",
  116. "黑龙江省": "黑龙江",
  117. "江苏省": "江苏",
  118. "浙江省": "浙江",
  119. "安徽省": "安徽",
  120. "福建省": "福建",
  121. "江西省": "江西",
  122. "山东省": "山东",
  123. "河南省": "河南",
  124. "湖北省": "湖北",
  125. "湖南省": "湖南",
  126. "广东省": "广东",
  127. "海南省": "海南",
  128. "四川省": "四川",
  129. "贵州省": "贵州",
  130. "云南省": "云南",
  131. "陕西省": "陕西",
  132. "甘肃省": "甘肃",
  133. "青海省": "青海",
  134. "台湾省": "台湾",
  135. // Autonomous regions (自治区) - use short form
  136. "内蒙古自治区": "内蒙古",
  137. "广西壮族自治区": "广西",
  138. "西藏自治区": "西藏",
  139. "宁夏回族自治区": "宁夏",
  140. "新疆维吾尔自治区": "新疆",
  141. // Special Administrative Regions (特别行政区) - use short form
  142. "香港特别行政区": "香港",
  143. "澳门特别行政区": "澳门",
  144. // Unknown
  145. "未知": "未知",
  146. }
  147. if shortName, exists := shortNameMap[fullName]; exists {
  148. return shortName
  149. }
  150. // If no mapping found, return the original name
  151. return fullName
  152. }
  153. // GetWorldMapData returns aggregated data for world map visualization
  154. func (s *BleveStatsService) GetWorldMapData(ctx context.Context, baseQuery query.Query) ([]WorldMapData, error) {
  155. regionCount := make(map[string]int)
  156. regionData := make(map[string]*WorldMapData)
  157. totalRequests := 0
  158. // Query all entries with more geographic fields
  159. searchReq := bleve.NewSearchRequest(baseQuery)
  160. searchReq.Size = 10000
  161. searchReq.Fields = []string{"region_code", "location", "province", "city", "isp"}
  162. from := 0
  163. for {
  164. searchReq.From = from
  165. searchResult, err := s.indexer.index.Search(searchReq)
  166. if err != nil {
  167. return nil, fmt.Errorf("failed to search logs: %w", err)
  168. }
  169. if len(searchResult.Hits) == 0 {
  170. break
  171. }
  172. for _, hit := range searchResult.Hits {
  173. regionCode := extractField(hit, "region_code")
  174. isp := extractField(hit, "isp")
  175. if regionCode == "" {
  176. regionCode = "UNKNOWN"
  177. }
  178. // Skip UNKNOWN entries for world map (they're not useful for geographic visualization)
  179. if regionCode == "UNKNOWN" {
  180. totalRequests++ // Still count for percentage calculation
  181. continue
  182. }
  183. // Unify Hong Kong, Macao, and Taiwan under China for world map display
  184. // but preserve original region code for detailed analysis
  185. regionCode = normalizeRegionForWorldMap(regionCode)
  186. // Initialize or update region data
  187. if _, exists := regionData[regionCode]; !exists {
  188. regionData[regionCode] = &WorldMapData{
  189. RegionCode: regionCode,
  190. ISP: isp,
  191. Value: 0,
  192. }
  193. }
  194. regionCount[regionCode]++
  195. totalRequests++
  196. }
  197. from += len(searchResult.Hits)
  198. if uint64(from) >= searchResult.Total {
  199. break
  200. }
  201. }
  202. // Convert to WorldMapData slice with calculated percentages
  203. var results []WorldMapData
  204. for code, count := range regionCount {
  205. percent := 0.0
  206. if totalRequests > 0 {
  207. percent = float64(count) * 100.0 / float64(totalRequests)
  208. }
  209. data := &WorldMapData{
  210. RegionCode: code,
  211. Value: count,
  212. Percent: percent,
  213. }
  214. results = append(results, *data)
  215. }
  216. // Sort by count (descending)
  217. sort.Slice(results, func(i, j int) bool {
  218. return results[i].Value > results[j].Value
  219. })
  220. return results, nil
  221. }
  222. // GetChinaMapData returns aggregated data for China map visualization
  223. func (s *BleveStatsService) GetChinaMapData(ctx context.Context, baseQuery query.Query) ([]ChinaMapData, error) {
  224. // Debug: First let's see what region codes are actually in the index
  225. allRegionsReq := bleve.NewSearchRequest(baseQuery)
  226. allRegionsReq.Size = 1000
  227. allRegionsReq.Fields = []string{"region_code"}
  228. // First, filter for Chinese IPs - use MatchQuery instead of TermQuery
  229. chineseRegions := []string{"CN", "HK", "MO", "TW"}
  230. regionQueries := make([]query.Query, 0, len(chineseRegions))
  231. for _, region := range chineseRegions {
  232. matchQuery := bleve.NewMatchQuery(region)
  233. matchQuery.SetField("region_code")
  234. regionQueries = append(regionQueries, matchQuery)
  235. }
  236. chinaQuery := bleve.NewDisjunctionQuery(regionQueries...)
  237. // Combine with base query if provided
  238. var finalQuery query.Query
  239. if baseQuery != nil {
  240. finalQuery = bleve.NewConjunctionQuery(baseQuery, chinaQuery)
  241. // Test the conjunction query first to see if it returns results
  242. testReq := bleve.NewSearchRequest(finalQuery)
  243. testReq.Size = 1
  244. testResult, testErr := s.indexer.index.Search(testReq)
  245. if testErr == nil && testResult.Total == 0 {
  246. finalQuery = chinaQuery
  247. }
  248. } else {
  249. finalQuery = chinaQuery
  250. }
  251. provinceData := make(map[string]*ChinaMapData)
  252. totalRequests := 0
  253. // Query Chinese entries
  254. searchReq := bleve.NewSearchRequest(finalQuery)
  255. searchReq.Size = 10000
  256. searchReq.Fields = []string{"region_code", "province", "city"}
  257. from := 0
  258. for {
  259. searchReq.From = from
  260. searchResult, err := s.indexer.index.Search(searchReq)
  261. if err != nil {
  262. return nil, fmt.Errorf("failed to search logs: %w", err)
  263. }
  264. if len(searchResult.Hits) == 0 {
  265. break
  266. }
  267. for _, hit := range searchResult.Hits {
  268. regionCode := ""
  269. province := ""
  270. city := ""
  271. // Get region code first
  272. if regField, ok := hit.Fields["region_code"]; ok {
  273. if reg, ok := regField.(string); ok {
  274. regionCode = reg
  275. }
  276. }
  277. if provField, ok := hit.Fields["province"]; ok {
  278. if prov, ok := provField.(string); ok && prov != "" && prov != "0" {
  279. province = prov
  280. }
  281. }
  282. if cityField, ok := hit.Fields["city"]; ok {
  283. if c, ok := cityField.(string); ok && c != "" && c != "0" {
  284. city = c
  285. }
  286. }
  287. // Handle special regions for China map
  288. switch regionCode {
  289. case "HK":
  290. province = "香港特别行政区"
  291. if city == "" {
  292. city = "香港"
  293. }
  294. case "MO":
  295. province = "澳门特别行政区"
  296. if city == "" {
  297. city = "澳门"
  298. }
  299. case "TW":
  300. province = "台湾省"
  301. if city == "" {
  302. city = "台北"
  303. }
  304. default:
  305. // For mainland China, normalize the province name
  306. if province == "" {
  307. province = "未知"
  308. } else {
  309. province = normalizeProvinceName(province)
  310. }
  311. }
  312. // Initialize province data if not exists
  313. if _, exists := provinceData[province]; !exists {
  314. provinceData[province] = &ChinaMapData{
  315. Name: getProvinceShortName(province), // Use short name for ECharts map display
  316. Value: 0,
  317. Cities: make([]CityData, 0),
  318. }
  319. }
  320. provinceData[province].Value++
  321. // Track city data if available
  322. if city != "" && city != province {
  323. found := false
  324. for i, cityData := range provinceData[province].Cities {
  325. if cityData.Name == city {
  326. provinceData[province].Cities[i].Value++
  327. found = true
  328. break
  329. }
  330. }
  331. if !found {
  332. provinceData[province].Cities = append(provinceData[province].Cities, CityData{
  333. Name: city,
  334. Value: 1,
  335. })
  336. }
  337. }
  338. totalRequests++
  339. }
  340. from += len(searchResult.Hits)
  341. if uint64(from) >= searchResult.Total {
  342. break
  343. }
  344. }
  345. // Convert to slice and calculate percentages
  346. var results []ChinaMapData
  347. for _, data := range provinceData {
  348. if totalRequests > 0 {
  349. data.Percent = float64(data.Value) * 100.0 / float64(totalRequests)
  350. // Calculate city percentages
  351. for i := range data.Cities {
  352. data.Cities[i].Percent = float64(data.Cities[i].Value) * 100.0 / float64(data.Value)
  353. }
  354. // Sort cities by value
  355. sort.Slice(data.Cities, func(i, j int) bool {
  356. return data.Cities[i].Value > data.Cities[j].Value
  357. })
  358. }
  359. results = append(results, *data)
  360. }
  361. // Sort provinces by value
  362. sort.Slice(results, func(i, j int) bool {
  363. return results[i].Value > results[j].Value
  364. })
  365. return results, nil
  366. }
  367. // GetGeoStats returns geographic statistics for the given query
  368. func (s *BleveStatsService) GetGeoStats(ctx context.Context, baseQuery query.Query, limit int) ([]GeoStats, error) {
  369. geoCount := make(map[string]*GeoStats)
  370. totalRequests := 0
  371. // Query all entries
  372. searchReq := bleve.NewSearchRequest(baseQuery)
  373. searchReq.Size = 10000
  374. searchReq.Fields = []string{"region_code", "location", "province", "city"}
  375. from := 0
  376. for {
  377. searchReq.From = from
  378. searchResult, err := s.indexer.index.Search(searchReq)
  379. if err != nil {
  380. return nil, fmt.Errorf("failed to search logs: %w", err)
  381. }
  382. if len(searchResult.Hits) == 0 {
  383. break
  384. }
  385. for _, hit := range searchResult.Hits {
  386. regionCode := extractField(hit, "region_code")
  387. location := extractField(hit, "location")
  388. province := extractField(hit, "province")
  389. city := extractField(hit, "city")
  390. if regionCode == "" {
  391. regionCode = "UNKNOWN"
  392. }
  393. key := regionCode
  394. if geolite.IsChineseRegion(regionCode) && province != "" {
  395. key = fmt.Sprintf("%s-%s", regionCode, province)
  396. }
  397. if _, exists := geoCount[key]; !exists {
  398. country := ""
  399. if location != "" {
  400. parts := strings.Split(location, ",")
  401. if len(parts) > 0 {
  402. country = strings.TrimSpace(parts[0])
  403. }
  404. }
  405. geoCount[key] = &GeoStats{
  406. RegionCode: regionCode,
  407. Country: country,
  408. Province: province,
  409. City: city,
  410. Count: 0,
  411. }
  412. }
  413. geoCount[key].Count++
  414. totalRequests++
  415. }
  416. from += len(searchResult.Hits)
  417. if uint64(from) >= searchResult.Total {
  418. break
  419. }
  420. }
  421. // Convert to slice and calculate percentages
  422. var results []GeoStats
  423. for _, stats := range geoCount {
  424. if totalRequests > 0 {
  425. stats.Percent = float64(stats.Count) * 100.0 / float64(totalRequests)
  426. }
  427. results = append(results, *stats)
  428. }
  429. // Sort by count (descending)
  430. sort.Slice(results, func(i, j int) bool {
  431. return results[i].Count > results[j].Count
  432. })
  433. // Apply limit if specified
  434. if limit > 0 && len(results) > limit {
  435. results = results[:limit]
  436. }
  437. return results, nil
  438. }
  439. // Helper function to extract field from search hit
  440. func extractField(hit *search.DocumentMatch, fieldName string) string {
  441. if field, ok := hit.Fields[fieldName]; ok {
  442. if value, ok := field.(string); ok && value != "" && value != "0" {
  443. return value
  444. }
  445. }
  446. return ""
  447. }