geo.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. package analytics
  2. import (
  3. "context"
  4. "fmt"
  5. "github.com/0xJacky/Nginx-UI/internal/nginx_log/searcher"
  6. "github.com/uozi-tech/cosy/logger"
  7. )
  8. func (s *service) GetGeoDistribution(ctx context.Context, req *GeoQueryRequest) (*GeoDistribution, error) {
  9. if req == nil {
  10. return nil, fmt.Errorf("request cannot be nil")
  11. }
  12. if err := s.ValidateTimeRange(req.StartTime, req.EndTime); err != nil {
  13. return nil, fmt.Errorf("invalid time range: %w", err)
  14. }
  15. logger.Debugf("=== DEBUG GetGeoDistribution START ===")
  16. logger.Debugf("GetGeoDistribution - req: %+v", req)
  17. searchReq := &searcher.SearchRequest{
  18. StartTime: &req.StartTime,
  19. EndTime: &req.EndTime,
  20. LogPaths: req.LogPaths,
  21. Limit: 0, // We only need facets.
  22. IncludeFacets: true,
  23. FacetFields: []string{"region_code"},
  24. FacetSize: 300, // Large enough to cover all countries
  25. UseCache: true,
  26. }
  27. logger.Debugf("GetGeoDistribution - SearchRequest: %+v", searchReq)
  28. result, err := s.searcher.Search(ctx, searchReq)
  29. if err != nil {
  30. logger.Debugf("GetGeoDistribution - Search failed: %v", err)
  31. return nil, fmt.Errorf("failed to get geo distribution: %w", err)
  32. }
  33. logger.Debugf("GetGeoDistribution - Search returned TotalHits: %d", result.TotalHits)
  34. logger.Debugf("GetGeoDistribution - Search returned %d facets", len(result.Facets))
  35. dist := &GeoDistribution{
  36. Countries: make(map[string]int),
  37. }
  38. if result.Facets != nil {
  39. if countryFacet, ok := result.Facets["region_code"]; ok {
  40. logger.Debugf("GetGeoDistribution - Found region_code facet with %d terms", len(countryFacet.Terms))
  41. for _, term := range countryFacet.Terms {
  42. if term.Term == "CN" {
  43. logger.Debugf("GetGeoDistribution - FOUND CN - Term: '%s', Count: %d", term.Term, term.Count)
  44. }
  45. logger.Debugf("GetGeoDistribution - Country term: '%s', Count: %d", term.Term, term.Count)
  46. dist.Countries[term.Term] = term.Count
  47. }
  48. } else {
  49. logger.Debugf("GetGeoDistribution - No 'region_code' facet found in result")
  50. for facetName := range result.Facets {
  51. logger.Debugf("GetGeoDistribution - Available facet: '%s'", facetName)
  52. }
  53. }
  54. } else {
  55. logger.Debugf("GetGeoDistribution - No facets in search result")
  56. }
  57. logger.Debugf("GetGeoDistribution - Final distribution has %d countries", len(dist.Countries))
  58. if cnCount, ok := dist.Countries["CN"]; ok {
  59. logger.Debugf("GetGeoDistribution - CN final count: %d", cnCount)
  60. }
  61. logger.Debugf("=== DEBUG GetGeoDistribution END ===")
  62. return dist, nil
  63. }
  64. func (s *service) GetGeoDistributionByCountry(ctx context.Context, req *GeoQueryRequest, countryCode string) (*GeoDistribution, error) {
  65. if req == nil {
  66. return nil, fmt.Errorf("request cannot be nil")
  67. }
  68. if err := s.ValidateTimeRange(req.StartTime, req.EndTime); err != nil {
  69. return nil, fmt.Errorf("invalid time range: %w", err)
  70. }
  71. logger.Debugf("=== DEBUG GetGeoDistributionByCountry START ===")
  72. logger.Debugf("GetGeoDistributionByCountry - countryCode: '%s'", countryCode)
  73. logger.Debugf("GetGeoDistributionByCountry - req: %+v", req)
  74. searchReq := &searcher.SearchRequest{
  75. StartTime: &req.StartTime,
  76. EndTime: &req.EndTime,
  77. LogPaths: req.LogPaths,
  78. Countries: []string{countryCode}, // Use proper country filter instead of text query
  79. Limit: 0, // We only need facets.
  80. IncludeFacets: true,
  81. FacetFields: []string{"province"},
  82. FacetSize: 100, // Large enough to cover all provinces in a country
  83. UseCache: true,
  84. }
  85. logger.Debugf("GetGeoDistributionByCountry - SearchRequest: %+v", searchReq)
  86. logger.Debugf("GetGeoDistributionByCountry - Countries filter: %v", searchReq.Countries)
  87. result, err := s.searcher.Search(ctx, searchReq)
  88. if err != nil {
  89. logger.Debugf("GetGeoDistributionByCountry - Search failed: %v", err)
  90. return nil, fmt.Errorf("failed to get geo distribution by country: %w", err)
  91. }
  92. logger.Debugf("GetGeoDistributionByCountry - Search returned TotalHits: %d", result.TotalHits)
  93. logger.Debugf("GetGeoDistributionByCountry - Search returned %d facets", len(result.Facets))
  94. dist := &GeoDistribution{
  95. Countries: make(map[string]int), // Reusing 'Countries' map for provinces
  96. }
  97. if result.Facets != nil {
  98. if provinceFacet, ok := result.Facets["province"]; ok {
  99. logger.Debugf("GetGeoDistributionByCountry - Found province facet with %d terms, Total: %d, Missing: %d", len(provinceFacet.Terms), provinceFacet.Total, provinceFacet.Missing)
  100. for _, term := range provinceFacet.Terms {
  101. logger.Debugf("GetGeoDistributionByCountry - Province term: '%s', Count: %d", term.Term, term.Count)
  102. dist.Countries[term.Term] = term.Count
  103. }
  104. } else {
  105. logger.Debugf("GetGeoDistributionByCountry - No 'province' facet found in result")
  106. for facetName, facet := range result.Facets {
  107. logger.Debugf("GetGeoDistributionByCountry - Available facet: '%s' (Total: %d, Missing: %d, Terms: %d)", facetName, facet.Total, facet.Missing, len(facet.Terms))
  108. }
  109. }
  110. } else {
  111. logger.Debugf("GetGeoDistributionByCountry - No facets in search result")
  112. }
  113. logger.Debugf("GetGeoDistributionByCountry - Final distribution has %d provinces", len(dist.Countries))
  114. logger.Debugf("=== DEBUG GetGeoDistributionByCountry END ===")
  115. return dist, nil
  116. }
  117. func (s *service) GetTopCountries(ctx context.Context, req *GeoQueryRequest) ([]CountryStats, error) {
  118. if req == nil {
  119. return nil, fmt.Errorf("request cannot be nil")
  120. }
  121. if err := s.ValidateTimeRange(req.StartTime, req.EndTime); err != nil {
  122. return nil, fmt.Errorf("invalid time range: %w", err)
  123. }
  124. searchReq := &searcher.SearchRequest{
  125. StartTime: &req.StartTime,
  126. EndTime: &req.EndTime,
  127. LogPaths: req.LogPaths,
  128. Limit: 0, // We only need facets
  129. IncludeFacets: true,
  130. FacetFields: []string{"region_code"},
  131. FacetSize: req.Limit, // Use the requested limit for facet size
  132. UseCache: true,
  133. }
  134. result, err := s.searcher.Search(ctx, searchReq)
  135. if err != nil {
  136. return nil, fmt.Errorf("failed to get top countries: %w", err)
  137. }
  138. var stats []CountryStats
  139. if result.Facets != nil {
  140. if countryFacet, ok := result.Facets["region_code"]; ok {
  141. for _, term := range countryFacet.Terms {
  142. stats = append(stats, CountryStats{
  143. Country: term.Term,
  144. Requests: term.Count,
  145. })
  146. }
  147. }
  148. }
  149. // Facets are already sorted by count descending from bleve
  150. return stats, nil
  151. }
  152. func (s *service) GetTopCities(ctx context.Context, req *GeoQueryRequest) ([]CityStats, error) {
  153. if req == nil {
  154. return nil, fmt.Errorf("request cannot be nil")
  155. }
  156. if err := s.ValidateTimeRange(req.StartTime, req.EndTime); err != nil {
  157. return nil, fmt.Errorf("invalid time range: %w", err)
  158. }
  159. searchReq := &searcher.SearchRequest{
  160. StartTime: &req.StartTime,
  161. EndTime: &req.EndTime,
  162. LogPaths: req.LogPaths,
  163. Limit: 0, // We only need facets
  164. IncludeFacets: true,
  165. FacetFields: []string{"city"},
  166. FacetSize: req.Limit,
  167. UseCache: true,
  168. }
  169. result, err := s.searcher.Search(ctx, searchReq)
  170. if err != nil {
  171. return nil, fmt.Errorf("failed to get top cities: %w", err)
  172. }
  173. var stats []CityStats
  174. if result.Facets != nil {
  175. if cityFacet, ok := result.Facets["city"]; ok {
  176. totalHits := int(result.TotalHits)
  177. for _, term := range cityFacet.Terms {
  178. percent := float64(term.Count) / float64(totalHits) * 100
  179. stats = append(stats, CityStats{
  180. City: term.Term,
  181. Count: term.Count,
  182. Percent: percent,
  183. })
  184. }
  185. }
  186. }
  187. return stats, nil
  188. }
  189. func (s *service) GetGeoStatsForIP(ctx context.Context, req *GeoQueryRequest, ip string) (*CityStats, error) {
  190. if req == nil {
  191. return nil, fmt.Errorf("request cannot be nil")
  192. }
  193. if ip == "" {
  194. return nil, fmt.Errorf("IP address cannot be empty")
  195. }
  196. if err := s.ValidateTimeRange(req.StartTime, req.EndTime); err != nil {
  197. return nil, fmt.Errorf("invalid time range: %w", err)
  198. }
  199. searchReq := &searcher.SearchRequest{
  200. StartTime: &req.StartTime,
  201. EndTime: &req.EndTime,
  202. LogPaths: req.LogPaths,
  203. Limit: 0,
  204. IncludeFacets: true,
  205. FacetFields: []string{"country", "country_code", "city"},
  206. FacetSize: 10,
  207. Query: fmt.Sprintf(`ip:"%s"`, ip),
  208. UseCache: true,
  209. }
  210. result, err := s.searcher.Search(ctx, searchReq)
  211. if err != nil {
  212. return nil, fmt.Errorf("failed to get geo stats for IP: %w", err)
  213. }
  214. if result.TotalHits == 0 {
  215. return nil, fmt.Errorf("no data found for IP %s", ip)
  216. }
  217. if result.Facets == nil {
  218. return nil, fmt.Errorf("could not extract geo information for IP %s", ip)
  219. }
  220. stats := &CityStats{
  221. Count: int(result.TotalHits),
  222. Percent: 100.0, // 100% for single IP
  223. }
  224. if countryFacet, ok := result.Facets["country"]; ok && len(countryFacet.Terms) > 0 {
  225. stats.Country = countryFacet.Terms[0].Term
  226. }
  227. if countryCodeFacet, ok := result.Facets["country_code"]; ok && len(countryCodeFacet.Terms) > 0 {
  228. stats.CountryCode = countryCodeFacet.Terms[0].Term
  229. }
  230. if cityFacet, ok := result.Facets["city"]; ok && len(cityFacet.Terms) > 0 {
  231. stats.City = cityFacet.Terms[0].Term
  232. }
  233. return stats, nil
  234. }