123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945 |
- package nginx_log
- import (
- "context"
- "net/http"
- "sort"
- "time"
- "github.com/0xJacky/Nginx-UI/internal/nginx"
- "github.com/0xJacky/Nginx-UI/internal/nginx_log"
- "github.com/0xJacky/Nginx-UI/internal/nginx_log/analytics"
- "github.com/0xJacky/Nginx-UI/internal/nginx_log/searcher"
- "github.com/gin-gonic/gin"
- "github.com/uozi-tech/cosy"
- "github.com/uozi-tech/cosy/logger"
- )
- type GeoRegionItem struct {
- Code string `json:"code"`
- Value int `json:"value"`
- Percent float64 `json:"percent"`
- }
- type GeoDataItem struct {
- Name string `json:"name"`
- Value int `json:"value"`
- Percent float64 `json:"percent"`
- }
- // AnalyticsRequest represents the request for log analytics
- type AnalyticsRequest struct {
- Path string `json:"path" form:"path"`
- StartTime int64 `json:"start_time" form:"start_time"`
- EndTime int64 `json:"end_time" form:"end_time"`
- Limit int `json:"limit" form:"limit"`
- }
- // AdvancedSearchRequest represents the request for advanced log search
- type AdvancedSearchRequest struct {
- Query string `json:"query" form:"query"`
- LogPath string `json:"log_path" form:"log_path"`
- StartTime int64 `json:"start_time" form:"start_time"`
- EndTime int64 `json:"end_time" form:"end_time"`
- IP string `json:"ip" form:"ip"`
- Method string `json:"method" form:"method"`
- Status []int `json:"status" form:"status"`
- Path string `json:"path" form:"path"`
- UserAgent string `json:"user_agent" form:"user_agent"`
- Referer string `json:"referer" form:"referer"`
- Browser string `json:"browser" form:"browser"`
- OS string `json:"os" form:"os"`
- Device string `json:"device" form:"device"`
- Limit int `json:"limit" form:"limit"`
- Offset int `json:"offset" form:"offset"`
- SortBy string `json:"sort_by" form:"sort_by"`
- SortOrder string `json:"sort_order" form:"sort_order"`
- }
- // SummaryStats Structures to match the frontend's expectations for the search response
- type SummaryStats struct {
- UV int `json:"uv"`
- PV int `json:"pv"`
- TotalTraffic int64 `json:"total_traffic"`
- UniquePages int `json:"unique_pages"`
- AvgTrafficPerPV float64 `json:"avg_traffic_per_pv"`
- }
- type AdvancedSearchResponseAPI struct {
- Entries []map[string]interface{} `json:"entries"`
- Total uint64 `json:"total"`
- Took int64 `json:"took"` // Milliseconds
- Query string `json:"query"`
- Summary SummaryStats `json:"summary"`
- }
- // PreflightResponse represents the response for preflight query
- // GetLogAnalytics provides comprehensive log analytics
- func GetLogAnalytics(c *gin.Context) {
- var req AnalyticsRequest
- if !cosy.BindAndValid(c, &req) {
- return
- }
- // Get modern analytics service
- analyticsService := nginx_log.GetAnalytics()
- if analyticsService == nil {
- cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
- return
- }
- // Validate log path
- if err := analyticsService.ValidateLogPath(req.Path); err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- // Build search request for log entries statistics
- searchReq := &searcher.SearchRequest{
- Limit: req.Limit,
- UseCache: true,
- IncludeStats: true,
- IncludeFacets: true,
- FacetFields: []string{"path", "ip", "user_agent", "status", "method"},
- }
- if req.StartTime > 0 {
- searchReq.StartTime = &req.StartTime
- }
- if req.EndTime > 0 {
- searchReq.EndTime = &req.EndTime
- }
- // Get log entries statistics
- ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
- defer cancel()
- stats, err := analyticsService.GetLogEntriesStats(ctx, searchReq)
- if err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- c.JSON(http.StatusOK, stats)
- }
- // GetLogPreflight returns the preflight status for log indexing
- func GetLogPreflight(c *gin.Context) {
- // Get optional log path parameter
- logPath := c.Query("log_path")
- // Create preflight service and perform check
- preflightService := nginx_log.NewPreflight()
- internalResponse, err := preflightService.CheckLogPreflight(logPath)
- if err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- // Convert internal response to API response
- response := PreflightResponse{
- Available: internalResponse.Available,
- IndexStatus: internalResponse.IndexStatus,
- Message: internalResponse.Message,
- }
- if internalResponse.TimeRange != nil {
- response.TimeRange = &TimeRange{
- Start: internalResponse.TimeRange.Start,
- End: internalResponse.TimeRange.End,
- }
- }
- if internalResponse.FileInfo != nil {
- response.FileInfo = &FileInfo{
- Exists: internalResponse.FileInfo.Exists,
- Readable: internalResponse.FileInfo.Readable,
- Size: internalResponse.FileInfo.Size,
- LastModified: internalResponse.FileInfo.LastModified,
- }
- }
- c.JSON(http.StatusOK, response)
- }
- // AdvancedSearchLogs provides advanced search capabilities for logs
- func AdvancedSearchLogs(c *gin.Context) {
- var req AdvancedSearchRequest
- if !cosy.BindAndValid(c, &req) {
- return
- }
- searcherService := nginx_log.GetSearcher()
- if searcherService == nil {
- cosy.ErrHandler(c, nginx_log.ErrModernSearcherNotAvailable)
- return
- }
- analyticsService := nginx_log.GetAnalytics()
- if analyticsService == nil {
- cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
- return
- }
- // Use default access log path if LogPath is empty
- if req.LogPath == "" {
- defaultLogPath := nginx.GetAccessLogPath()
- if defaultLogPath != "" {
- req.LogPath = defaultLogPath
- logger.Debugf("Using default access log path for search: %s", req.LogPath)
- }
- }
- // Validate log path if provided
- if req.LogPath != "" {
- if err := analyticsService.ValidateLogPath(req.LogPath); err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- }
- // Build search request
- searchReq := &searcher.SearchRequest{
- Query: req.Query,
- Limit: req.Limit,
- Offset: req.Offset,
- SortBy: req.SortBy,
- SortOrder: req.SortOrder,
- UseCache: true,
- Timeout: 60 * time.Second, // Add timeout for large facet operations
- IncludeHighlighting: true,
- IncludeFacets: true, // Re-enable facets for accurate summary stats
- FacetFields: []string{"ip", "path_exact"}, // For UV and Unique Pages
- FacetSize: 10000, // Balanced: large enough for most cases, but not excessive
- }
- // If no sorting is specified, default to sorting by timestamp descending.
- if searchReq.SortBy == "" {
- searchReq.SortBy = "timestamp"
- searchReq.SortOrder = "desc"
- }
- // Expand the base log path to all physical files in the group using filesystem globbing.
- if req.LogPath != "" {
- logPaths, err := nginx_log.ExpandLogGroupPath(req.LogPath)
- if err != nil {
- logger.Warnf("Could not expand log group path %s: %v", req.LogPath, err)
- // Fallback to using the raw path when expansion fails
- searchReq.LogPaths = []string{req.LogPath}
- } else if len(logPaths) == 0 {
- // ExpandLogGroupPath succeeded but returned empty slice (file doesn't exist on filesystem)
- // Still search for historical indexed data using the requested path
- logger.Debugf("Log file %s does not exist on filesystem, but searching for historical indexed data", req.LogPath)
- searchReq.LogPaths = []string{req.LogPath}
- } else {
- searchReq.LogPaths = logPaths
- }
- logger.Debugf("Search request LogPaths: %v", searchReq.LogPaths)
- }
- // Add time filters
- if req.StartTime > 0 {
- searchReq.StartTime = &req.StartTime
- }
- if req.EndTime > 0 {
- searchReq.EndTime = &req.EndTime
- }
- // If no time range is provided, default to searching all time.
- if searchReq.StartTime == nil && searchReq.EndTime == nil {
- var startTime int64 = 0 // Unix epoch
- now := time.Now().Unix()
- searchReq.StartTime = &startTime
- searchReq.EndTime = &now
- }
- // Add field filters
- if req.IP != "" {
- searchReq.IPAddresses = []string{req.IP}
- }
- if req.Method != "" {
- searchReq.Methods = []string{req.Method}
- }
- if req.Path != "" {
- searchReq.Paths = []string{req.Path}
- }
- if req.UserAgent != "" {
- searchReq.UserAgents = []string{req.UserAgent}
- }
- if req.Referer != "" {
- searchReq.Referers = []string{req.Referer}
- }
- if req.Browser != "" {
- searchReq.Browsers = []string{req.Browser}
- }
- if req.OS != "" {
- searchReq.OSs = []string{req.OS}
- }
- if req.Device != "" {
- searchReq.Devices = []string{req.Device}
- }
- if len(req.Status) > 0 {
- searchReq.StatusCodes = req.Status
- }
- // Execute search with timeout
- ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Minute)
- defer cancel()
- result, err := searcherService.Search(ctx, searchReq)
- if err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- // --- Transform the searcher result to the API response structure ---
- // 1. Extract entries from hits
- entries := make([]map[string]interface{}, len(result.Hits))
- var totalTraffic int64 // Total traffic is for the entire result set, must be calculated separately if needed.
- for i, hit := range result.Hits {
- entries[i] = hit.Fields
- if bytesSent, ok := hit.Fields["bytes_sent"].(float64); ok {
- totalTraffic += int64(bytesSent)
- }
- }
- // 2. Calculate summary stats from the overall results using Counter for accuracy
- pv := int(result.TotalHits)
- var uv, uniquePages int
- var facetUV, facetUniquePages int
- // First get facet values as fallback
- if result.Facets != nil {
- if ipFacet, ok := result.Facets["ip"]; ok {
- facetUV = ipFacet.Total // .Total on a facet gives the count of unique terms
- uv = facetUV
- }
- if pathFacet, ok := result.Facets["path_exact"]; ok {
- facetUniquePages = pathFacet.Total
- uniquePages = facetUniquePages
- }
- }
- // Override with Counter results for better accuracy
- if analyticsService != nil {
- // Get cardinality counts for UV (unique IPs)
- if uvResult := getCardinalityCount(ctx, "ip", searchReq); uvResult > 0 {
- uv = uvResult
- logger.Debugf("🔢 Search endpoint - UV from Counter: %d (vs facet: %d)", uvResult, facetUV)
- }
- // Get cardinality counts for Unique Pages (unique paths)
- if upResult := getCardinalityCount(ctx, "path_exact", searchReq); upResult > 0 {
- uniquePages = upResult
- logger.Debugf("🔢 Search endpoint - Unique Pages from Counter: %d (vs facet: %d)", upResult, facetUniquePages)
- }
- }
- // Note: TotalTraffic is not available for the whole result set without a separate query.
- // We will approximate it based on the current page's average for now.
- var avgBytesOnPage float64
- if len(result.Hits) > 0 {
- avgBytesOnPage = float64(totalTraffic) / float64(len(result.Hits))
- }
- approximatedTotalTraffic := int64(avgBytesOnPage * float64(pv))
- var avgTraffic float64
- if pv > 0 {
- avgTraffic = float64(approximatedTotalTraffic) / float64(pv)
- }
- summary := SummaryStats{
- UV: uv,
- PV: pv,
- TotalTraffic: approximatedTotalTraffic,
- UniquePages: uniquePages,
- AvgTrafficPerPV: avgTraffic,
- }
- // 3. Assemble the final response
- apiResponse := AdvancedSearchResponseAPI{
- Entries: entries,
- Total: result.TotalHits,
- Took: result.Duration.Milliseconds(),
- Query: req.Query,
- Summary: summary,
- }
- c.JSON(http.StatusOK, apiResponse)
- }
- // GetLogEntries provides simple log entry retrieval
- func GetLogEntries(c *gin.Context) {
- var req struct {
- Path string `json:"path" form:"path"`
- Limit int `json:"limit" form:"limit"`
- Tail bool `json:"tail" form:"tail"` // Get latest entries
- }
- if !cosy.BindAndValid(c, &req) {
- return
- }
- searcherService := nginx_log.GetSearcher()
- if searcherService == nil {
- cosy.ErrHandler(c, nginx_log.ErrModernSearcherNotAvailable)
- return
- }
- analyticsService := nginx_log.GetAnalytics()
- if analyticsService == nil {
- cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
- return
- }
- // Validate log path
- if err := analyticsService.ValidateLogPath(req.Path); err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- // Set default limit
- if req.Limit == 0 {
- req.Limit = 100
- }
- // Build search request
- searchReq := &searcher.SearchRequest{
- Limit: req.Limit,
- UseCache: false, // Don't cache simple entry requests
- SortBy: "timestamp",
- SortOrder: "desc", // Latest first by default
- }
- if req.Tail {
- searchReq.SortOrder = "desc" // Latest entries first
- } else {
- searchReq.SortOrder = "asc" // Oldest entries first
- }
- // Execute search
- ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
- defer cancel()
- result, err := searcherService.Search(ctx, searchReq)
- if err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- // Convert search hits to simple entries format
- var entries []map[string]interface{}
- for _, hit := range result.Hits {
- entries = append(entries, hit.Fields)
- }
- c.JSON(http.StatusOK, AnalyticsResponse{
- Entries: entries,
- Count: len(entries),
- })
- }
- // DashboardRequest represents the request for dashboard analytics
- type DashboardRequest struct {
- LogPath string `json:"log_path" form:"log_path"`
- StartDate string `json:"start_date" form:"start_date"` // Format: 2006-01-02
- EndDate string `json:"end_date" form:"end_date"` // Format: 2006-01-02
- }
- // HourlyStats represents hourly UV/PV statistics
- type HourlyStats struct {
- Hour int `json:"hour"` // 0-23
- UV int `json:"uv"` // Unique visitors (unique IPs)
- PV int `json:"pv"` // Page views (total requests)
- Timestamp int64 `json:"timestamp"` // Unix timestamp for the hour
- }
- // DailyStats represents daily access statistics
- type DailyStats struct {
- Date string `json:"date"` // YYYY-MM-DD format
- UV int `json:"uv"` // Unique visitors
- PV int `json:"pv"` // Page views
- Timestamp int64 `json:"timestamp"` // Unix timestamp for the day
- }
- // URLStats represents URL access statistics
- type URLStats struct {
- URL string `json:"url"`
- Visits int `json:"visits"`
- Percent float64 `json:"percent"`
- }
- // BrowserStats represents browser statistics
- type BrowserStats struct {
- Browser string `json:"browser"`
- Count int `json:"count"`
- Percent float64 `json:"percent"`
- }
- // OSStats represents operating system statistics
- type OSStats struct {
- OS string `json:"os"`
- Count int `json:"count"`
- Percent float64 `json:"percent"`
- }
- // DeviceStats represents device type statistics
- type DeviceStats struct {
- Device string `json:"device"`
- Count int `json:"count"`
- Percent float64 `json:"percent"`
- }
- // DashboardResponse represents the dashboard analytics response
- type DashboardResponse struct {
- HourlyStats []HourlyStats `json:"hourly_stats"` // 24-hour UV/PV data
- DailyStats []DailyStats `json:"daily_stats"` // Monthly trend data
- TopURLs []URLStats `json:"top_urls"` // TOP 10 URLs
- Browsers []BrowserStats `json:"browsers"` // Browser statistics
- OperatingSystems []OSStats `json:"operating_systems"` // OS statistics
- Devices []DeviceStats `json:"devices"` // Device statistics
- Summary struct {
- TotalUV int `json:"total_uv"` // Total unique visitors
- TotalPV int `json:"total_pv"` // Total page views
- AvgDailyUV float64 `json:"avg_daily_uv"` // Average daily UV
- AvgDailyPV float64 `json:"avg_daily_pv"` // Average daily PV
- PeakHour int `json:"peak_hour"` // Peak traffic hour (0-23)
- PeakHourTraffic int `json:"peak_hour_traffic"` // Peak hour PV count
- } `json:"summary"`
- }
- // GetDashboardAnalytics provides comprehensive dashboard analytics from modern analytics service
- func GetDashboardAnalytics(c *gin.Context) {
- var req DashboardRequest
- // Parse JSON body for POST request
- if err := c.ShouldBindJSON(&req); err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- logger.Debugf("Dashboard API received log_path: '%s', start_date: '%s', end_date: '%s'", req.LogPath, req.StartDate, req.EndDate)
- analyticsService := nginx_log.GetAnalytics()
- if analyticsService == nil {
- cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
- return
- }
- // Use default access log path if LogPath is empty
- if req.LogPath == "" {
- defaultLogPath := nginx.GetAccessLogPath()
- if defaultLogPath != "" {
- req.LogPath = defaultLogPath
- logger.Debugf("Using default access log path: %s", req.LogPath)
- }
- }
- // Validate log path if provided
- if req.LogPath != "" {
- if err := analyticsService.ValidateLogPath(req.LogPath); err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- }
- // Parse and validate date strings
- var startTime, endTime time.Time
- var err error
- if req.StartDate != "" {
- startTime, err = time.Parse("2006-01-02", req.StartDate)
- if err != nil {
- c.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid start_date format, expected YYYY-MM-DD: " + err.Error()})
- return
- }
- // Convert to UTC for consistent processing
- startTime = startTime.UTC()
- }
- if req.EndDate != "" {
- endTime, err = time.Parse("2006-01-02", req.EndDate)
- if err != nil {
- c.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid end_date format, expected YYYY-MM-DD: " + err.Error()})
- return
- }
- // Set end time to end of day and convert to UTC
- endTime = endTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second).UTC()
- }
- // Set default time range if not provided (last 30 days)
- if startTime.IsZero() || endTime.IsZero() {
- endTime = time.Now()
- startTime = endTime.AddDate(0, 0, -30) // 30 days ago
- }
- // Get dashboard analytics with timeout
- ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
- defer cancel()
- logger.Debugf("Dashboard request for log_path: %s, parsed start_time: %v, end_time: %v", req.LogPath, startTime, endTime)
- // Use main_log_path field for efficient log group queries instead of expanding file paths
- // This provides much better performance by using indexed field filtering
- logger.Debugf("Dashboard querying log group with main_log_path: %s", req.LogPath)
- // Build dashboard query request
- dashboardReq := &analytics.DashboardQueryRequest{
- LogPath: req.LogPath,
- LogPaths: []string{req.LogPath}, // Use single main log path
- StartTime: startTime.Unix(),
- EndTime: endTime.Unix(),
- }
- logger.Debugf("Query parameters - LogPath='%s', StartTime=%v, EndTime=%v",
- dashboardReq.LogPath, dashboardReq.StartTime, dashboardReq.EndTime)
- // Get analytics from modern analytics service
- result, err := analyticsService.GetDashboardAnalytics(ctx, dashboardReq)
- if err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- logger.Debugf("Successfully retrieved dashboard analytics")
- // Debug: Log summary of results
- if result != nil {
- logger.Debugf("Results summary - TotalUV=%d, TotalPV=%d, HourlyStats=%d, DailyStats=%d, TopURLs=%d",
- result.Summary.TotalUV, result.Summary.TotalPV,
- len(result.HourlyStats), len(result.DailyStats), len(result.TopURLs))
- } else {
- logger.Debugf("Analytics result is nil")
- }
- c.JSON(http.StatusOK, result)
- }
- // GetWorldMapData provides geographic data for world map visualization
- func GetWorldMapData(c *gin.Context) {
- var req AnalyticsRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- logger.Debugf("=== DEBUG GetWorldMapData START ===")
- logger.Debugf("WorldMapData request - Path: '%s', StartTime: %d, EndTime: %d, Limit: %d",
- req.Path, req.StartTime, req.EndTime, req.Limit)
- analyticsService := nginx_log.GetAnalytics()
- if analyticsService == nil {
- cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
- return
- }
- // Use default access log path if Path is empty
- if req.Path == "" {
- defaultLogPath := nginx.GetAccessLogPath()
- if defaultLogPath != "" {
- req.Path = defaultLogPath
- logger.Debugf("Using default access log path for world map: %s", req.Path)
- }
- }
- // Validate log path if provided
- if req.Path != "" {
- if err := analyticsService.ValidateLogPath(req.Path); err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- }
- // Use main_log_path field for efficient log group queries instead of expanding file paths
- logger.Debugf("WorldMapData - Using main_log_path field for log group: %s", req.Path)
- // Get world map data with timeout
- ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
- defer cancel()
- geoReq := &analytics.GeoQueryRequest{
- StartTime: req.StartTime,
- EndTime: req.EndTime,
- LogPath: req.Path,
- LogPaths: []string{req.Path}, // Use single main log path
- UseMainLogPath: true, // Use main_log_path field for efficient queries
- Limit: req.Limit,
- }
- logger.Debugf("WorldMapData - GeoQueryRequest: %+v", geoReq)
- data, err := analyticsService.GetGeoDistribution(ctx, geoReq)
- if err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- logger.Debugf("WorldMapData - GetGeoDistribution returned data with %d countries", len(data.Countries))
- for code, count := range data.Countries {
- if code == "CN" {
- logger.Debugf("WorldMapData - CN country count: %d", count)
- }
- logger.Debugf("WorldMapData - Country: '%s', Count: %d", code, count)
- }
- // Transform map to slice for frontend chart compatibility, calculate percentages, and sort.
- chartData := make([]GeoRegionItem, 0, len(data.Countries))
- totalValue := 0
- for _, value := range data.Countries {
- totalValue += value
- }
- logger.Debugf("WorldMapData - Total value calculated: %d", totalValue)
- for code, value := range data.Countries {
- percent := 0.0
- if totalValue > 0 {
- percent = (float64(value) / float64(totalValue)) * 100
- }
- chartData = append(chartData, GeoRegionItem{Code: code, Value: value, Percent: percent})
- }
- // Sort by value descending
- sort.Slice(chartData, func(i, j int) bool {
- return chartData[i].Value > chartData[j].Value
- })
- logger.Debugf("WorldMapData - Final response data contains %d items with total value %d", len(chartData), totalValue)
- for i, item := range chartData {
- if item.Code == "CN" {
- logger.Debugf("WorldMapData - FOUND CN - [%d] Code: '%s', Value: %d, Percent: %.2f%%", i, item.Code, item.Value, item.Percent)
- }
- logger.Debugf("WorldMapData - [%d] Code: '%s', Value: %d, Percent: %.2f%%", i, item.Code, item.Value, item.Percent)
- }
- logger.Debugf("=== DEBUG GetWorldMapData END ===")
- c.JSON(http.StatusOK, GeoRegionResponse{
- Data: chartData,
- })
- }
- // GetChinaMapData provides geographic data for China map visualization
- func GetChinaMapData(c *gin.Context) {
- var req AnalyticsRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- logger.Debugf("=== DEBUG GetChinaMapData START ===")
- logger.Debugf("ChinaMapData request - Path: '%s', StartTime: %d, EndTime: %d, Limit: %d",
- req.Path, req.StartTime, req.EndTime, req.Limit)
- analyticsService := nginx_log.GetAnalytics()
- if analyticsService == nil {
- cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
- return
- }
- // Use default access log path if Path is empty
- if req.Path == "" {
- defaultLogPath := nginx.GetAccessLogPath()
- if defaultLogPath != "" {
- req.Path = defaultLogPath
- logger.Debugf("Using default access log path for China map: %s", req.Path)
- }
- }
- // Validate log path if provided
- if req.Path != "" {
- if err := analyticsService.ValidateLogPath(req.Path); err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- }
- // Use main_log_path field for efficient log group queries instead of expanding file paths
- logger.Debugf("ChinaMapData - Using main_log_path field for log group: %s", req.Path)
- // Get China map data with timeout
- ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
- defer cancel()
- geoReq := &analytics.GeoQueryRequest{
- StartTime: req.StartTime,
- EndTime: req.EndTime,
- LogPath: req.Path,
- LogPaths: []string{req.Path}, // Use single main log path
- UseMainLogPath: true, // Use main_log_path field for efficient queries
- Limit: req.Limit,
- }
- logger.Debugf("ChinaMapData - GeoQueryRequest: %+v", geoReq)
- // Get distribution specifically for China (country code "CN")
- logger.Debugf("ChinaMapData - About to call GetGeoDistributionByCountry with country code 'CN'")
- data, err := analyticsService.GetGeoDistributionByCountry(ctx, geoReq, "CN")
- if err != nil {
- logger.Debugf("ChinaMapData - GetGeoDistributionByCountry returned error: %v", err)
- cosy.ErrHandler(c, err)
- return
- }
- logger.Debugf("ChinaMapData - GetGeoDistributionByCountry returned data with %d provinces", len(data.Countries))
- for name, count := range data.Countries {
- logger.Debugf("ChinaMapData - Province: '%s', Count: %d", name, count)
- }
- // Transform map to slice for frontend chart compatibility, calculate percentages, and sort.
- chartData := make([]GeoDataItem, 0, len(data.Countries))
- totalValue := 0
- for _, value := range data.Countries {
- totalValue += value
- }
- logger.Debugf("ChinaMapData - Total value calculated: %d", totalValue)
- for name, value := range data.Countries {
- percent := 0.0
- if totalValue > 0 {
- percent = (float64(value) / float64(totalValue)) * 100
- }
- chartData = append(chartData, GeoDataItem{Name: name, Value: value, Percent: percent})
- }
- // Sort by value descending
- sort.Slice(chartData, func(i, j int) bool {
- return chartData[i].Value > chartData[j].Value
- })
- logger.Debugf("ChinaMapData - Final response data contains %d items with total value %d", len(chartData), totalValue)
- for i, item := range chartData {
- logger.Debugf("ChinaMapData - [%d] Name: '%s', Value: %d, Percent: %.2f%%", i, item.Name, item.Value, item.Percent)
- }
- logger.Debugf("=== DEBUG GetChinaMapData END ===")
- c.JSON(http.StatusOK, GeoDataResponse{
- Data: chartData,
- })
- }
- // GetGeoStats provides geographic statistics
- func GetGeoStats(c *gin.Context) {
- var req AnalyticsRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- c.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid JSON request body: " + err.Error()})
- return
- }
- analyticsService := nginx_log.GetAnalytics()
- if analyticsService == nil {
- cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
- return
- }
- // Use default access log path if Path is empty
- if req.Path == "" {
- defaultLogPath := nginx.GetAccessLogPath()
- if defaultLogPath != "" {
- req.Path = defaultLogPath
- logger.Debugf("Using default access log path for geo stats: %s", req.Path)
- }
- }
- // Validate log path if provided
- if req.Path != "" {
- if err := analyticsService.ValidateLogPath(req.Path); err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- }
- // Use main_log_path field for efficient log group queries instead of expanding file paths
- logger.Debugf("GeoStats - Using main_log_path field for log group: %s", req.Path)
- // Set default limit if not provided
- if req.Limit == 0 {
- req.Limit = 20
- }
- // Get geographic statistics with timeout
- ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
- defer cancel()
- geoReq := &analytics.GeoQueryRequest{
- StartTime: req.StartTime,
- EndTime: req.EndTime,
- LogPath: req.Path,
- LogPaths: []string{req.Path}, // Use single main log path
- UseMainLogPath: true, // Use main_log_path field for efficient queries
- Limit: req.Limit,
- }
- stats, err := analyticsService.GetTopCountries(ctx, geoReq)
- if err != nil {
- cosy.ErrHandler(c, err)
- return
- }
- // Convert to []interface{} for JSON serialization
- statsInterface := make([]interface{}, len(stats))
- for i, stat := range stats {
- statsInterface[i] = stat
- }
-
- c.JSON(http.StatusOK, GeoStatsResponse{
- Stats: statsInterface,
- })
- }
- // getCardinalityCount is a helper function to get accurate cardinality counts using the analytics service
- func getCardinalityCount(ctx context.Context, field string, searchReq *searcher.SearchRequest) int {
- logger.Debugf("🔍 getCardinalityCount: Starting cardinality count for field '%s'", field)
- // Create a CardinalityRequest from the SearchRequest
- cardReq := &searcher.CardinalityRequest{
- Field: field,
- StartTime: searchReq.StartTime,
- EndTime: searchReq.EndTime,
- LogPaths: searchReq.LogPaths,
- UseMainLogPath: searchReq.UseMainLogPath, // Use main_log_path field if enabled
- }
- logger.Debugf("🔍 CardinalityRequest: Field=%s, StartTime=%v, EndTime=%v, LogPaths=%v",
- cardReq.Field, cardReq.StartTime, cardReq.EndTime, cardReq.LogPaths)
- // Try to get the searcher to access cardinality counter
- searcherService := nginx_log.GetSearcher()
- if searcherService == nil {
- logger.Debugf("🚨 getCardinalityCount: ModernSearcher not available for field %s", field)
- return 0
- }
- logger.Debugf("🔍 getCardinalityCount: ModernSearcher available, type: %T", searcherService)
- // Use searcher to access Counter
- if searcherService != nil {
- ds := searcherService
- logger.Debugf("🔍 getCardinalityCount: Successfully cast to Searcher")
- shards := ds.GetShards()
- logger.Debugf("🔍 getCardinalityCount: Retrieved %d shards", len(shards))
- if len(shards) > 0 {
- // Check shard health
- for i, shard := range shards {
- logger.Debugf("🔍 getCardinalityCount: Shard %d: %v", i, shard != nil)
- }
- cardinalityCounter := searcher.NewCounter(shards)
- logger.Debugf("🔍 getCardinalityCount: Created Counter")
- result, err := cardinalityCounter.Count(ctx, cardReq)
- if err != nil {
- logger.Debugf("🚨 getCardinalityCount: Counter failed for field %s: %v", field, err)
- return 0
- }
- if result.Error != "" {
- logger.Debugf("🚨 getCardinalityCount: Counter returned error for field %s: %s", field, result.Error)
- return 0
- }
- logger.Debugf("✅ getCardinalityCount: Successfully got cardinality for field %s: %d", field, result.Cardinality)
- return int(result.Cardinality)
- } else {
- logger.Debugf("🚨 getCardinalityCount: Searcher has no shards for field %s", field)
- }
- } else {
- logger.Debugf("🚨 getCardinalityCount: Searcher is not Searcher (type: %T) for field %s", searcherService, field)
- }
- return 0
- }
|