geo.go 9.4 KB

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