123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- 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
- }
|