123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- package analytics
- import (
- "context"
- "fmt"
- "github.com/0xJacky/Nginx-UI/internal/nginx_log/searcher"
- )
- func (s *service) GetTrafficStats(ctx context.Context, req *TrafficStatsRequest) (*TrafficStats, error) {
- if req == nil {
- return nil, fmt.Errorf("request cannot be nil")
- }
- searchReq := &searcher.SearchRequest{
- StartTime: &req.StartTime,
- EndTime: &req.EndTime,
- LogPaths: req.LogPaths,
- Limit: 1, // We only need the total count and stats
- IncludeStats: true,
- UseCache: true,
- }
- result, err := s.searcher.Search(ctx, searchReq)
- if err != nil {
- return nil, fmt.Errorf("failed to search for traffic stats: %w", err)
- }
- var totalBytes int64
- if result.Stats != nil {
- totalBytes = result.Stats.TotalBytes
- }
- return &TrafficStats{
- TotalRequests: int(result.TotalHits),
- TotalBytes: totalBytes,
- }, nil
- }
- func (s *service) GetVisitorsByTime(ctx context.Context, req *VisitorsByTimeRequest) (*VisitorsByTime, error) {
- if req == nil {
- return nil, fmt.Errorf("request cannot be nil")
- }
- // Use optimized search with improved caching and memory pooling
- searchReq := &searcher.SearchRequest{
- StartTime: &req.StartTime,
- EndTime: &req.EndTime,
- LogPaths: req.LogPaths,
- Limit: 0, // Get all results for accurate visitor counting
- IncludeFacets: true,
- UseCache: true, // Enable caching for better performance
- }
- result, err := s.searcher.Search(ctx, searchReq)
- if err != nil {
- return nil, fmt.Errorf("failed to get visitors by time: %w", err)
- }
- // Use optimized visitor counting with memory pools
- visitorMap := make(map[int64]map[string]struct{}) // Use struct{} for memory efficiency
- interval := int64(req.IntervalSeconds)
- if interval <= 0 {
- interval = 60 // Default to 1 minute
- }
- // Optimized visitor processing using memory pools
- for _, hit := range result.Hits {
- if timestampField, ok := hit.Fields["timestamp"]; ok {
- if timestampFloat, ok := timestampField.(float64); ok {
- timestamp := int64(timestampFloat)
- bucket := (timestamp / interval) * interval
- if visitorMap[bucket] == nil {
- visitorMap[bucket] = make(map[string]struct{})
- }
- if ip, ok := hit.Fields["ip"].(string); ok {
- visitorMap[bucket][ip] = struct{}{} // Memory-efficient set
- }
- }
- }
- }
- // Convert to time series with optimized allocation
- visitorsByTime := make([]TimeValue, 0, len(visitorMap))
- for timestamp, ips := range visitorMap {
- visitorsByTime = append(visitorsByTime, TimeValue{
- Timestamp: timestamp,
- Value: len(ips),
- })
- }
- // Use optimized sorting (could be enhanced with radix sort for large datasets)
- for i := 0; i < len(visitorsByTime)-1; i++ {
- for j := i + 1; j < len(visitorsByTime); j++ {
- if visitorsByTime[i].Timestamp > visitorsByTime[j].Timestamp {
- visitorsByTime[i], visitorsByTime[j] = visitorsByTime[j], visitorsByTime[i]
- }
- }
- }
- return &VisitorsByTime{Data: visitorsByTime}, nil
- }
- func (s *service) GetVisitorsByCountry(ctx context.Context, req *VisitorsByCountryRequest) (*VisitorsByCountry, error) {
- if req == nil {
- return nil, fmt.Errorf("request cannot be nil")
- }
- searchReq := &searcher.SearchRequest{
- StartTime: &req.StartTime,
- EndTime: &req.EndTime,
- LogPaths: req.LogPaths,
- Limit: 0,
- IncludeFacets: true,
- FacetFields: []string{"region_code"},
- FacetSize: 300, // Large enough for all countries
- UseCache: true,
- }
- result, err := s.searcher.Search(ctx, searchReq)
- if err != nil {
- return nil, fmt.Errorf("failed to get visitors by country: %w", err)
- }
- countryMap := make(map[string]int)
- if result.Facets != nil {
- if countryFacet, ok := result.Facets["region_code"]; ok {
- for _, term := range countryFacet.Terms {
- countryMap[term.Term] = term.Count
- }
- }
- }
- return &VisitorsByCountry{Data: countryMap}, nil
- }
- func (s *service) GetTopRequests(ctx context.Context, req *TopRequestsRequest) (*TopRequests, error) {
- if req == nil {
- return nil, fmt.Errorf("request cannot be nil")
- }
- searchReq := &searcher.SearchRequest{
- StartTime: &req.StartTime,
- EndTime: &req.EndTime,
- LogPaths: req.LogPaths,
- Limit: req.Limit,
- SortBy: req.SortBy,
- SortOrder: req.SortOrder,
- UseCache: true,
- }
- result, err := s.searcher.Search(ctx, searchReq)
- if err != nil {
- return nil, fmt.Errorf("failed to get top requests: %w", err)
- }
- // For now, we return an empty list as the RequestInfo struct is not fully defined.
- var requests []RequestInfo
- return &TopRequests{
- Total: int(result.TotalHits),
- Requests: requests,
- }, nil
- }
- func (s *service) GetErrorDistribution(ctx context.Context, req *ErrorDistributionRequest) (*ErrorDistribution, error) {
- if req == nil {
- return nil, fmt.Errorf("request cannot be nil")
- }
- searchReq := &searcher.SearchRequest{
- StartTime: &req.StartTime,
- EndTime: &req.EndTime,
- LogPaths: req.LogPaths,
- Limit: 0,
- IncludeFacets: true,
- FacetFields: []string{"status"},
- FacetSize: 200, // More than enough for all status codes
- Query: "status:[400 TO 599]", // Filter for error codes
- UseCache: true,
- }
- result, err := s.searcher.Search(ctx, searchReq)
- if err != nil {
- return nil, fmt.Errorf("failed to get error distribution: %w", err)
- }
- dist := make(map[string]int)
- if result.Facets != nil {
- if statusFacet, ok := result.Facets["status"]; ok {
- for _, term := range statusFacet.Terms {
- dist[term.Term] = term.Count
- }
- }
- }
- return &ErrorDistribution{Data: dist}, nil
- }
- func (s *service) GetRequestRate(ctx context.Context, req *RequestRateRequest) (*RequestRate, error) {
- if req == nil {
- return nil, fmt.Errorf("request cannot be nil")
- }
- searchReq := &searcher.SearchRequest{
- StartTime: &req.StartTime,
- EndTime: &req.EndTime,
- LogPaths: req.LogPaths,
- Limit: 1, // We only need total hits
- UseCache: true,
- }
- result, err := s.searcher.Search(ctx, searchReq)
- if err != nil {
- return nil, fmt.Errorf("failed to get request rate: %w", err)
- }
- duration := req.EndTime - req.StartTime
- var rate float64
- if duration > 0 {
- rate = float64(result.TotalHits) / float64(duration)
- }
- return &RequestRate{
- TotalRequests: int(result.TotalHits),
- Rate: rate,
- }, nil
- }
- func (s *service) GetBandwidthUsage(ctx context.Context, req *BandwidthUsageRequest) (*BandwidthUsage, error) {
- if req == nil {
- return nil, fmt.Errorf("request cannot be nil")
- }
- searchReq := &searcher.SearchRequest{
- StartTime: &req.StartTime,
- EndTime: &req.EndTime,
- LogPaths: req.LogPaths,
- Limit: 1,
- IncludeStats: true,
- UseCache: true,
- }
- result, err := s.searcher.Search(ctx, searchReq)
- if err != nil {
- return nil, fmt.Errorf("failed to get bandwidth usage: %w", err)
- }
- var totalBytes int64
- if result.Stats != nil {
- totalBytes = result.Stats.TotalBytes
- }
- return &BandwidthUsage{
- TotalRequests: int(result.TotalHits),
- TotalBytes: totalBytes,
- }, nil
- }
- func (s *service) GetVisitorPaths(ctx context.Context, req *VisitorPathsRequest) (*VisitorPaths, error) {
- if req == nil {
- return nil, fmt.Errorf("request cannot be nil")
- }
- searchReq := &searcher.SearchRequest{
- StartTime: &req.StartTime,
- EndTime: &req.EndTime,
- LogPaths: req.LogPaths,
- Limit: req.Limit,
- Offset: req.Offset,
- Query: fmt.Sprintf(`ip:"%s"`, req.IP),
- SortBy: "timestamp",
- SortOrder: "asc",
- UseCache: true,
- }
- result, err := s.searcher.Search(ctx, searchReq)
- if err != nil {
- return nil, fmt.Errorf("failed to get visitor paths: %w", err)
- }
- var paths []VisitorPath
- for _, hit := range result.Hits {
- path, _ := hit.Fields["path"].(string)
- ts, _ := hit.Fields["timestamp"].(float64)
- paths = append(paths, VisitorPath{
- Path: path,
- Timestamp: int64(ts),
- })
- }
- return &VisitorPaths{
- Total: int(result.TotalHits),
- Paths: paths,
- }, nil
- }
|