package analytics import ( "context" "fmt" "github.com/0xJacky/Nginx-UI/internal/nginx_log/searcher" "github.com/uozi-tech/cosy/logger" ) func (s *service) GetGeoDistribution(ctx context.Context, req *GeoQueryRequest) (*GeoDistribution, error) { if req == nil { return nil, fmt.Errorf("request cannot be nil") } if err := s.ValidateTimeRange(req.StartTime, req.EndTime); err != nil { return nil, fmt.Errorf("invalid time range: %w", err) } logger.Debugf("=== DEBUG GetGeoDistribution START ===") logger.Debugf("GetGeoDistribution - req: %+v", req) searchReq := &searcher.SearchRequest{ StartTime: &req.StartTime, EndTime: &req.EndTime, LogPaths: req.LogPaths, UseMainLogPath: req.UseMainLogPath, // Use main_log_path field for efficient queries Limit: 0, // We only need facets. IncludeFacets: true, FacetFields: []string{"region_code"}, FacetSize: 300, // Large enough to cover all countries UseCache: true, } logger.Debugf("GetGeoDistribution - SearchRequest: %+v", searchReq) result, err := s.searcher.Search(ctx, searchReq) if err != nil { logger.Debugf("GetGeoDistribution - Search failed: %v", err) return nil, fmt.Errorf("failed to get geo distribution: %w", err) } logger.Debugf("GetGeoDistribution - Search returned TotalHits: %d", result.TotalHits) logger.Debugf("GetGeoDistribution - Search returned %d facets", len(result.Facets)) dist := &GeoDistribution{ Countries: make(map[string]int), } if result.Facets != nil { if countryFacet, ok := result.Facets["region_code"]; ok { logger.Debugf("GetGeoDistribution - Found region_code facet with %d terms", len(countryFacet.Terms)) for _, term := range countryFacet.Terms { if term.Term == "CN" { logger.Debugf("GetGeoDistribution - FOUND CN - Term: '%s', Count: %d", term.Term, term.Count) } logger.Debugf("GetGeoDistribution - Country term: '%s', Count: %d", term.Term, term.Count) dist.Countries[term.Term] = term.Count } } else { logger.Debugf("GetGeoDistribution - No 'region_code' facet found in result") for facetName := range result.Facets { logger.Debugf("GetGeoDistribution - Available facet: '%s'", facetName) } } } else { logger.Debugf("GetGeoDistribution - No facets in search result") } logger.Debugf("GetGeoDistribution - Final distribution has %d countries", len(dist.Countries)) if cnCount, ok := dist.Countries["CN"]; ok { logger.Debugf("GetGeoDistribution - CN final count: %d", cnCount) } logger.Debugf("=== DEBUG GetGeoDistribution END ===") return dist, nil } func (s *service) GetGeoDistributionByCountry(ctx context.Context, req *GeoQueryRequest, countryCode string) (*GeoDistribution, error) { if req == nil { return nil, fmt.Errorf("request cannot be nil") } if err := s.ValidateTimeRange(req.StartTime, req.EndTime); err != nil { return nil, fmt.Errorf("invalid time range: %w", err) } logger.Debugf("=== DEBUG GetGeoDistributionByCountry START ===") logger.Debugf("GetGeoDistributionByCountry - countryCode: '%s'", countryCode) logger.Debugf("GetGeoDistributionByCountry - req: %+v", req) searchReq := &searcher.SearchRequest{ StartTime: &req.StartTime, EndTime: &req.EndTime, LogPaths: req.LogPaths, UseMainLogPath: req.UseMainLogPath, // Use main_log_path field for efficient queries Countries: []string{countryCode}, // Use proper country filter instead of text query Limit: 0, // We only need facets. IncludeFacets: true, FacetFields: []string{"province"}, FacetSize: 100, // Large enough to cover all provinces in a country UseCache: true, } logger.Debugf("GetGeoDistributionByCountry - SearchRequest: %+v", searchReq) logger.Debugf("GetGeoDistributionByCountry - Countries filter: %v", searchReq.Countries) result, err := s.searcher.Search(ctx, searchReq) if err != nil { logger.Debugf("GetGeoDistributionByCountry - Search failed: %v", err) return nil, fmt.Errorf("failed to get geo distribution by country: %w", err) } logger.Debugf("GetGeoDistributionByCountry - Search returned TotalHits: %d", result.TotalHits) logger.Debugf("GetGeoDistributionByCountry - Search returned %d facets", len(result.Facets)) dist := &GeoDistribution{ Countries: make(map[string]int), // Reusing 'Countries' map for provinces } if result.Facets != nil { if provinceFacet, ok := result.Facets["province"]; ok { logger.Debugf("GetGeoDistributionByCountry - Found province facet with %d terms, Total: %d, Missing: %d", len(provinceFacet.Terms), provinceFacet.Total, provinceFacet.Missing) for _, term := range provinceFacet.Terms { logger.Debugf("GetGeoDistributionByCountry - Province term: '%s', Count: %d", term.Term, term.Count) dist.Countries[term.Term] = term.Count } } else { logger.Debugf("GetGeoDistributionByCountry - No 'province' facet found in result") for facetName, facet := range result.Facets { logger.Debugf("GetGeoDistributionByCountry - Available facet: '%s' (Total: %d, Missing: %d, Terms: %d)", facetName, facet.Total, facet.Missing, len(facet.Terms)) } } } else { logger.Debugf("GetGeoDistributionByCountry - No facets in search result") } logger.Debugf("GetGeoDistributionByCountry - Final distribution has %d provinces", len(dist.Countries)) logger.Debugf("=== DEBUG GetGeoDistributionByCountry END ===") return dist, nil } func (s *service) GetTopCountries(ctx context.Context, req *GeoQueryRequest) ([]CountryStats, error) { if req == nil { return nil, fmt.Errorf("request cannot be nil") } if err := s.ValidateTimeRange(req.StartTime, req.EndTime); err != nil { return nil, fmt.Errorf("invalid time range: %w", err) } searchReq := &searcher.SearchRequest{ StartTime: &req.StartTime, EndTime: &req.EndTime, LogPaths: req.LogPaths, UseMainLogPath: req.UseMainLogPath, // Use main_log_path field for efficient queries Limit: 0, // We only need facets IncludeFacets: true, FacetFields: []string{"region_code"}, FacetSize: req.Limit, // Use the requested limit for facet size UseCache: true, } result, err := s.searcher.Search(ctx, searchReq) if err != nil { return nil, fmt.Errorf("failed to get top countries: %w", err) } var stats []CountryStats if result.Facets != nil { if countryFacet, ok := result.Facets["region_code"]; ok { for _, term := range countryFacet.Terms { stats = append(stats, CountryStats{ Country: term.Term, Requests: term.Count, }) } } } // Facets are already sorted by count descending from bleve return stats, nil } func (s *service) GetTopCities(ctx context.Context, req *GeoQueryRequest) ([]CityStats, error) { if req == nil { return nil, fmt.Errorf("request cannot be nil") } if err := s.ValidateTimeRange(req.StartTime, req.EndTime); err != nil { return nil, fmt.Errorf("invalid time range: %w", err) } searchReq := &searcher.SearchRequest{ StartTime: &req.StartTime, EndTime: &req.EndTime, LogPaths: req.LogPaths, UseMainLogPath: req.UseMainLogPath, // Use main_log_path field for efficient queries Limit: 0, // We only need facets IncludeFacets: true, FacetFields: []string{"city"}, FacetSize: req.Limit, UseCache: true, } result, err := s.searcher.Search(ctx, searchReq) if err != nil { return nil, fmt.Errorf("failed to get top cities: %w", err) } var stats []CityStats if result.Facets != nil { if cityFacet, ok := result.Facets["city"]; ok { totalHits := int(result.TotalHits) for _, term := range cityFacet.Terms { percent := float64(term.Count) / float64(totalHits) * 100 stats = append(stats, CityStats{ City: term.Term, Count: term.Count, Percent: percent, }) } } } return stats, nil } func (s *service) GetGeoStatsForIP(ctx context.Context, req *GeoQueryRequest, ip string) (*CityStats, error) { if req == nil { return nil, fmt.Errorf("request cannot be nil") } if ip == "" { return nil, fmt.Errorf("IP address cannot be empty") } if err := s.ValidateTimeRange(req.StartTime, req.EndTime); err != nil { return nil, fmt.Errorf("invalid time range: %w", err) } searchReq := &searcher.SearchRequest{ StartTime: &req.StartTime, EndTime: &req.EndTime, LogPaths: req.LogPaths, UseMainLogPath: req.UseMainLogPath, // Use main_log_path field for efficient queries Limit: 0, IncludeFacets: true, FacetFields: []string{"country", "country_code", "city"}, FacetSize: 10, Query: fmt.Sprintf(`ip:"%s"`, ip), UseCache: true, } result, err := s.searcher.Search(ctx, searchReq) if err != nil { return nil, fmt.Errorf("failed to get geo stats for IP: %w", err) } if result.TotalHits == 0 { return nil, fmt.Errorf("no data found for IP %s", ip) } if result.Facets == nil { return nil, fmt.Errorf("could not extract geo information for IP %s", ip) } stats := &CityStats{ Count: int(result.TotalHits), Percent: 100.0, // 100% for single IP } if countryFacet, ok := result.Facets["country"]; ok && len(countryFacet.Terms) > 0 { stats.Country = countryFacet.Terms[0].Term } if countryCodeFacet, ok := result.Facets["country_code"]; ok && len(countryCodeFacet.Terms) > 0 { stats.CountryCode = countryCodeFacet.Terms[0].Term } if cityFacet, ok := result.Facets["city"]; ok && len(cityFacet.Terms) > 0 { stats.City = cityFacet.Terms[0].Term } return stats, nil }