| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 | package analyticsimport (	"context"	"fmt"	"github.com/0xJacky/Nginx-UI/internal/nginx_log/searcher"	"github.com/0xJacky/Nginx-UI/internal/nginx_log/utils")// Service defines the interface for analytics operationstype Service interface {	GetDashboardAnalytics(ctx context.Context, req *DashboardQueryRequest) (*DashboardAnalytics, error)	GetLogEntriesStats(ctx context.Context, req *searcher.SearchRequest) (*EntriesStats, error)	GetGeoDistribution(ctx context.Context, req *GeoQueryRequest) (*GeoDistribution, error)	GetGeoDistributionByCountry(ctx context.Context, req *GeoQueryRequest, countryCode string) (*GeoDistribution, error)	GetTopCountries(ctx context.Context, req *GeoQueryRequest) ([]CountryStats, error)	GetTopCities(ctx context.Context, req *GeoQueryRequest) ([]CityStats, error)	GetGeoStatsForIP(ctx context.Context, req *GeoQueryRequest, ip string) (*CityStats, error)	GetTopPaths(ctx context.Context, req *TopListRequest) ([]KeyValue, error)	GetTopIPs(ctx context.Context, req *TopListRequest) ([]KeyValue, error)	GetTopUserAgents(ctx context.Context, req *TopListRequest) ([]KeyValue, error)	ValidateLogPath(logPath string) error	ValidateTimeRange(startTime, endTime int64) error	Stop() error}// service implements the Service interfacetype service struct {	searcher           searcher.SearcherInterface	cardinalityCounter *searcher.Counter}// NewService creates a new analytics servicefunc NewService(s searcher.SearcherInterface) Service {	// Try to extract shards from distributed searcher for cardinality counting	var cardinalityCounter *searcher.Counter	if ds, ok := s.(*searcher.Searcher); ok {		shards := ds.GetShards()		if len(shards) > 0 {			cardinalityCounter = searcher.NewCounter(shards)		}	}	return &service{		searcher:           s,		cardinalityCounter: cardinalityCounter,	}}// Stop gracefully stops the analytics service and its componentsfunc (s *service) Stop() error {	if s.cardinalityCounter != nil {		return s.cardinalityCounter.Stop()	}	return nil}// getCardinalityCounter dynamically creates or returns a cardinality counter// This is necessary because shards may be updated after service initializationfunc (s *service) getCardinalityCounter() *searcher.Counter {	// If we already have a cardinality counter and it's still valid, use it	if s.cardinalityCounter != nil {		return s.cardinalityCounter	}	// Try to create a new cardinality counter from current shards	if ds, ok := s.searcher.(*searcher.Searcher); ok {		shards := ds.GetShards()		if len(shards) > 0 {			// Update our cached cardinality counter			s.cardinalityCounter = searcher.NewCounter(shards)			return s.cardinalityCounter		}	}	return nil}// ValidateLogPath validates the log path against whitelistfunc (s *service) ValidateLogPath(logPath string) error {	if logPath == "" {		return nil // Empty path is acceptable for global search	}	if !utils.IsValidLogPath(logPath) {		return fmt.Errorf("log path is not under whitelist")	}	return nil}// ValidateTimeRange validates the time range parametersfunc (s *service) ValidateTimeRange(startTime, endTime int64) error {	if startTime < 0 || endTime < 0 {		return fmt.Errorf("time values cannot be negative")	}	if startTime > 0 && endTime > 0 && startTime >= endTime {		return fmt.Errorf("start time must be before end time")	}	return nil}// buildBaseSearchRequest builds a base search request with common parametersfunc (s *service) buildBaseSearchRequest(startTime, endTime int64, logPath string) *searcher.SearchRequest {	req := &searcher.SearchRequest{		Limit:    DefaultLimit,		Offset:   0,		UseCache: true,	}	if startTime > 0 {		req.StartTime = &startTime	}	if endTime > 0 {		req.EndTime = &endTime	}	if logPath != "" {		req.LogPaths = []string{logPath}	}	return req}// validateAndNormalizeSearchRequest validates and normalizes a search requestfunc (s *service) validateAndNormalizeSearchRequest(req *searcher.SearchRequest) error {	if req == nil {		return fmt.Errorf("request cannot be nil")	}	if req.Limit <= 0 {		req.Limit = DefaultLimit	}	if req.Limit > MaxLimit {		req.Limit = MaxLimit	}	if req.Offset < 0 {		req.Offset = 0	}	return nil}
 |