1
0

analytics.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945
  1. package nginx_log
  2. import (
  3. "context"
  4. "net/http"
  5. "sort"
  6. "time"
  7. "github.com/0xJacky/Nginx-UI/internal/nginx"
  8. "github.com/0xJacky/Nginx-UI/internal/nginx_log"
  9. "github.com/0xJacky/Nginx-UI/internal/nginx_log/analytics"
  10. "github.com/0xJacky/Nginx-UI/internal/nginx_log/searcher"
  11. "github.com/gin-gonic/gin"
  12. "github.com/uozi-tech/cosy"
  13. "github.com/uozi-tech/cosy/logger"
  14. )
  15. type GeoRegionItem struct {
  16. Code string `json:"code"`
  17. Value int `json:"value"`
  18. Percent float64 `json:"percent"`
  19. }
  20. type GeoDataItem struct {
  21. Name string `json:"name"`
  22. Value int `json:"value"`
  23. Percent float64 `json:"percent"`
  24. }
  25. // AnalyticsRequest represents the request for log analytics
  26. type AnalyticsRequest struct {
  27. Path string `json:"path" form:"path"`
  28. StartTime int64 `json:"start_time" form:"start_time"`
  29. EndTime int64 `json:"end_time" form:"end_time"`
  30. Limit int `json:"limit" form:"limit"`
  31. }
  32. // AdvancedSearchRequest represents the request for advanced log search
  33. type AdvancedSearchRequest struct {
  34. Query string `json:"query" form:"query"`
  35. LogPath string `json:"log_path" form:"log_path"`
  36. StartTime int64 `json:"start_time" form:"start_time"`
  37. EndTime int64 `json:"end_time" form:"end_time"`
  38. IP string `json:"ip" form:"ip"`
  39. Method string `json:"method" form:"method"`
  40. Status []int `json:"status" form:"status"`
  41. Path string `json:"path" form:"path"`
  42. UserAgent string `json:"user_agent" form:"user_agent"`
  43. Referer string `json:"referer" form:"referer"`
  44. Browser string `json:"browser" form:"browser"`
  45. OS string `json:"os" form:"os"`
  46. Device string `json:"device" form:"device"`
  47. Limit int `json:"limit" form:"limit"`
  48. Offset int `json:"offset" form:"offset"`
  49. SortBy string `json:"sort_by" form:"sort_by"`
  50. SortOrder string `json:"sort_order" form:"sort_order"`
  51. }
  52. // SummaryStats Structures to match the frontend's expectations for the search response
  53. type SummaryStats struct {
  54. UV int `json:"uv"`
  55. PV int `json:"pv"`
  56. TotalTraffic int64 `json:"total_traffic"`
  57. UniquePages int `json:"unique_pages"`
  58. AvgTrafficPerPV float64 `json:"avg_traffic_per_pv"`
  59. }
  60. type AdvancedSearchResponseAPI struct {
  61. Entries []map[string]interface{} `json:"entries"`
  62. Total uint64 `json:"total"`
  63. Took int64 `json:"took"` // Milliseconds
  64. Query string `json:"query"`
  65. Summary SummaryStats `json:"summary"`
  66. }
  67. // PreflightResponse represents the response for preflight query
  68. // GetLogAnalytics provides comprehensive log analytics
  69. func GetLogAnalytics(c *gin.Context) {
  70. var req AnalyticsRequest
  71. if !cosy.BindAndValid(c, &req) {
  72. return
  73. }
  74. // Get modern analytics service
  75. analyticsService := nginx_log.GetAnalytics()
  76. if analyticsService == nil {
  77. cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
  78. return
  79. }
  80. // Validate log path
  81. if err := analyticsService.ValidateLogPath(req.Path); err != nil {
  82. cosy.ErrHandler(c, err)
  83. return
  84. }
  85. // Build search request for log entries statistics
  86. searchReq := &searcher.SearchRequest{
  87. Limit: req.Limit,
  88. UseCache: true,
  89. IncludeStats: true,
  90. IncludeFacets: true,
  91. FacetFields: []string{"path", "ip", "user_agent", "status", "method"},
  92. }
  93. if req.StartTime > 0 {
  94. searchReq.StartTime = &req.StartTime
  95. }
  96. if req.EndTime > 0 {
  97. searchReq.EndTime = &req.EndTime
  98. }
  99. // Get log entries statistics
  100. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
  101. defer cancel()
  102. stats, err := analyticsService.GetLogEntriesStats(ctx, searchReq)
  103. if err != nil {
  104. cosy.ErrHandler(c, err)
  105. return
  106. }
  107. c.JSON(http.StatusOK, stats)
  108. }
  109. // GetLogPreflight returns the preflight status for log indexing
  110. func GetLogPreflight(c *gin.Context) {
  111. // Get optional log path parameter
  112. logPath := c.Query("log_path")
  113. // Create preflight service and perform check
  114. preflightService := nginx_log.NewPreflight()
  115. internalResponse, err := preflightService.CheckLogPreflight(logPath)
  116. if err != nil {
  117. cosy.ErrHandler(c, err)
  118. return
  119. }
  120. // Convert internal response to API response
  121. response := PreflightResponse{
  122. Available: internalResponse.Available,
  123. IndexStatus: internalResponse.IndexStatus,
  124. Message: internalResponse.Message,
  125. }
  126. if internalResponse.TimeRange != nil {
  127. response.TimeRange = &TimeRange{
  128. Start: internalResponse.TimeRange.Start,
  129. End: internalResponse.TimeRange.End,
  130. }
  131. }
  132. if internalResponse.FileInfo != nil {
  133. response.FileInfo = &FileInfo{
  134. Exists: internalResponse.FileInfo.Exists,
  135. Readable: internalResponse.FileInfo.Readable,
  136. Size: internalResponse.FileInfo.Size,
  137. LastModified: internalResponse.FileInfo.LastModified,
  138. }
  139. }
  140. c.JSON(http.StatusOK, response)
  141. }
  142. // AdvancedSearchLogs provides advanced search capabilities for logs
  143. func AdvancedSearchLogs(c *gin.Context) {
  144. var req AdvancedSearchRequest
  145. if !cosy.BindAndValid(c, &req) {
  146. return
  147. }
  148. searcherService := nginx_log.GetSearcher()
  149. if searcherService == nil {
  150. cosy.ErrHandler(c, nginx_log.ErrModernSearcherNotAvailable)
  151. return
  152. }
  153. analyticsService := nginx_log.GetAnalytics()
  154. if analyticsService == nil {
  155. cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
  156. return
  157. }
  158. // Use default access log path if LogPath is empty
  159. if req.LogPath == "" {
  160. defaultLogPath := nginx.GetAccessLogPath()
  161. if defaultLogPath != "" {
  162. req.LogPath = defaultLogPath
  163. logger.Debugf("Using default access log path for search: %s", req.LogPath)
  164. }
  165. }
  166. // Validate log path if provided
  167. if req.LogPath != "" {
  168. if err := analyticsService.ValidateLogPath(req.LogPath); err != nil {
  169. cosy.ErrHandler(c, err)
  170. return
  171. }
  172. }
  173. // Build search request
  174. searchReq := &searcher.SearchRequest{
  175. Query: req.Query,
  176. Limit: req.Limit,
  177. Offset: req.Offset,
  178. SortBy: req.SortBy,
  179. SortOrder: req.SortOrder,
  180. UseCache: true,
  181. Timeout: 60 * time.Second, // Add timeout for large facet operations
  182. IncludeHighlighting: true,
  183. IncludeFacets: true, // Re-enable facets for accurate summary stats
  184. FacetFields: []string{"ip", "path_exact"}, // For UV and Unique Pages
  185. FacetSize: 10000, // Balanced: large enough for most cases, but not excessive
  186. }
  187. // If no sorting is specified, default to sorting by timestamp descending.
  188. if searchReq.SortBy == "" {
  189. searchReq.SortBy = "timestamp"
  190. searchReq.SortOrder = "desc"
  191. }
  192. // Expand the base log path to all physical files in the group using filesystem globbing.
  193. if req.LogPath != "" {
  194. logPaths, err := nginx_log.ExpandLogGroupPath(req.LogPath)
  195. if err != nil {
  196. logger.Warnf("Could not expand log group path %s: %v", req.LogPath, err)
  197. // Fallback to using the raw path when expansion fails
  198. searchReq.LogPaths = []string{req.LogPath}
  199. } else if len(logPaths) == 0 {
  200. // ExpandLogGroupPath succeeded but returned empty slice (file doesn't exist on filesystem)
  201. // Still search for historical indexed data using the requested path
  202. logger.Debugf("Log file %s does not exist on filesystem, but searching for historical indexed data", req.LogPath)
  203. searchReq.LogPaths = []string{req.LogPath}
  204. } else {
  205. searchReq.LogPaths = logPaths
  206. }
  207. logger.Debugf("Search request LogPaths: %v", searchReq.LogPaths)
  208. }
  209. // Add time filters
  210. if req.StartTime > 0 {
  211. searchReq.StartTime = &req.StartTime
  212. }
  213. if req.EndTime > 0 {
  214. searchReq.EndTime = &req.EndTime
  215. }
  216. // If no time range is provided, default to searching all time.
  217. if searchReq.StartTime == nil && searchReq.EndTime == nil {
  218. var startTime int64 = 0 // Unix epoch
  219. now := time.Now().Unix()
  220. searchReq.StartTime = &startTime
  221. searchReq.EndTime = &now
  222. }
  223. // Add field filters
  224. if req.IP != "" {
  225. searchReq.IPAddresses = []string{req.IP}
  226. }
  227. if req.Method != "" {
  228. searchReq.Methods = []string{req.Method}
  229. }
  230. if req.Path != "" {
  231. searchReq.Paths = []string{req.Path}
  232. }
  233. if req.UserAgent != "" {
  234. searchReq.UserAgents = []string{req.UserAgent}
  235. }
  236. if req.Referer != "" {
  237. searchReq.Referers = []string{req.Referer}
  238. }
  239. if req.Browser != "" {
  240. searchReq.Browsers = []string{req.Browser}
  241. }
  242. if req.OS != "" {
  243. searchReq.OSs = []string{req.OS}
  244. }
  245. if req.Device != "" {
  246. searchReq.Devices = []string{req.Device}
  247. }
  248. if len(req.Status) > 0 {
  249. searchReq.StatusCodes = req.Status
  250. }
  251. // Execute search with timeout
  252. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Minute)
  253. defer cancel()
  254. result, err := searcherService.Search(ctx, searchReq)
  255. if err != nil {
  256. cosy.ErrHandler(c, err)
  257. return
  258. }
  259. // --- Transform the searcher result to the API response structure ---
  260. // 1. Extract entries from hits
  261. entries := make([]map[string]interface{}, len(result.Hits))
  262. var totalTraffic int64 // Total traffic is for the entire result set, must be calculated separately if needed.
  263. for i, hit := range result.Hits {
  264. entries[i] = hit.Fields
  265. if bytesSent, ok := hit.Fields["bytes_sent"].(float64); ok {
  266. totalTraffic += int64(bytesSent)
  267. }
  268. }
  269. // 2. Calculate summary stats from the overall results using Counter for accuracy
  270. pv := int(result.TotalHits)
  271. var uv, uniquePages int
  272. var facetUV, facetUniquePages int
  273. // First get facet values as fallback
  274. if result.Facets != nil {
  275. if ipFacet, ok := result.Facets["ip"]; ok {
  276. facetUV = ipFacet.Total // .Total on a facet gives the count of unique terms
  277. uv = facetUV
  278. }
  279. if pathFacet, ok := result.Facets["path_exact"]; ok {
  280. facetUniquePages = pathFacet.Total
  281. uniquePages = facetUniquePages
  282. }
  283. }
  284. // Override with Counter results for better accuracy
  285. if analyticsService != nil {
  286. // Get cardinality counts for UV (unique IPs)
  287. if uvResult := getCardinalityCount(ctx, "ip", searchReq); uvResult > 0 {
  288. uv = uvResult
  289. logger.Debugf("🔢 Search endpoint - UV from Counter: %d (vs facet: %d)", uvResult, facetUV)
  290. }
  291. // Get cardinality counts for Unique Pages (unique paths)
  292. if upResult := getCardinalityCount(ctx, "path_exact", searchReq); upResult > 0 {
  293. uniquePages = upResult
  294. logger.Debugf("🔢 Search endpoint - Unique Pages from Counter: %d (vs facet: %d)", upResult, facetUniquePages)
  295. }
  296. }
  297. // Note: TotalTraffic is not available for the whole result set without a separate query.
  298. // We will approximate it based on the current page's average for now.
  299. var avgBytesOnPage float64
  300. if len(result.Hits) > 0 {
  301. avgBytesOnPage = float64(totalTraffic) / float64(len(result.Hits))
  302. }
  303. approximatedTotalTraffic := int64(avgBytesOnPage * float64(pv))
  304. var avgTraffic float64
  305. if pv > 0 {
  306. avgTraffic = float64(approximatedTotalTraffic) / float64(pv)
  307. }
  308. summary := SummaryStats{
  309. UV: uv,
  310. PV: pv,
  311. TotalTraffic: approximatedTotalTraffic,
  312. UniquePages: uniquePages,
  313. AvgTrafficPerPV: avgTraffic,
  314. }
  315. // 3. Assemble the final response
  316. apiResponse := AdvancedSearchResponseAPI{
  317. Entries: entries,
  318. Total: result.TotalHits,
  319. Took: result.Duration.Milliseconds(),
  320. Query: req.Query,
  321. Summary: summary,
  322. }
  323. c.JSON(http.StatusOK, apiResponse)
  324. }
  325. // GetLogEntries provides simple log entry retrieval
  326. func GetLogEntries(c *gin.Context) {
  327. var req struct {
  328. Path string `json:"path" form:"path"`
  329. Limit int `json:"limit" form:"limit"`
  330. Tail bool `json:"tail" form:"tail"` // Get latest entries
  331. }
  332. if !cosy.BindAndValid(c, &req) {
  333. return
  334. }
  335. searcherService := nginx_log.GetSearcher()
  336. if searcherService == nil {
  337. cosy.ErrHandler(c, nginx_log.ErrModernSearcherNotAvailable)
  338. return
  339. }
  340. analyticsService := nginx_log.GetAnalytics()
  341. if analyticsService == nil {
  342. cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
  343. return
  344. }
  345. // Validate log path
  346. if err := analyticsService.ValidateLogPath(req.Path); err != nil {
  347. cosy.ErrHandler(c, err)
  348. return
  349. }
  350. // Set default limit
  351. if req.Limit == 0 {
  352. req.Limit = 100
  353. }
  354. // Build search request
  355. searchReq := &searcher.SearchRequest{
  356. Limit: req.Limit,
  357. UseCache: false, // Don't cache simple entry requests
  358. SortBy: "timestamp",
  359. SortOrder: "desc", // Latest first by default
  360. }
  361. if req.Tail {
  362. searchReq.SortOrder = "desc" // Latest entries first
  363. } else {
  364. searchReq.SortOrder = "asc" // Oldest entries first
  365. }
  366. // Execute search
  367. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
  368. defer cancel()
  369. result, err := searcherService.Search(ctx, searchReq)
  370. if err != nil {
  371. cosy.ErrHandler(c, err)
  372. return
  373. }
  374. // Convert search hits to simple entries format
  375. var entries []map[string]interface{}
  376. for _, hit := range result.Hits {
  377. entries = append(entries, hit.Fields)
  378. }
  379. c.JSON(http.StatusOK, AnalyticsResponse{
  380. Entries: entries,
  381. Count: len(entries),
  382. })
  383. }
  384. // DashboardRequest represents the request for dashboard analytics
  385. type DashboardRequest struct {
  386. LogPath string `json:"log_path" form:"log_path"`
  387. StartDate string `json:"start_date" form:"start_date"` // Format: 2006-01-02
  388. EndDate string `json:"end_date" form:"end_date"` // Format: 2006-01-02
  389. }
  390. // HourlyStats represents hourly UV/PV statistics
  391. type HourlyStats struct {
  392. Hour int `json:"hour"` // 0-23
  393. UV int `json:"uv"` // Unique visitors (unique IPs)
  394. PV int `json:"pv"` // Page views (total requests)
  395. Timestamp int64 `json:"timestamp"` // Unix timestamp for the hour
  396. }
  397. // DailyStats represents daily access statistics
  398. type DailyStats struct {
  399. Date string `json:"date"` // YYYY-MM-DD format
  400. UV int `json:"uv"` // Unique visitors
  401. PV int `json:"pv"` // Page views
  402. Timestamp int64 `json:"timestamp"` // Unix timestamp for the day
  403. }
  404. // URLStats represents URL access statistics
  405. type URLStats struct {
  406. URL string `json:"url"`
  407. Visits int `json:"visits"`
  408. Percent float64 `json:"percent"`
  409. }
  410. // BrowserStats represents browser statistics
  411. type BrowserStats struct {
  412. Browser string `json:"browser"`
  413. Count int `json:"count"`
  414. Percent float64 `json:"percent"`
  415. }
  416. // OSStats represents operating system statistics
  417. type OSStats struct {
  418. OS string `json:"os"`
  419. Count int `json:"count"`
  420. Percent float64 `json:"percent"`
  421. }
  422. // DeviceStats represents device type statistics
  423. type DeviceStats struct {
  424. Device string `json:"device"`
  425. Count int `json:"count"`
  426. Percent float64 `json:"percent"`
  427. }
  428. // DashboardResponse represents the dashboard analytics response
  429. type DashboardResponse struct {
  430. HourlyStats []HourlyStats `json:"hourly_stats"` // 24-hour UV/PV data
  431. DailyStats []DailyStats `json:"daily_stats"` // Monthly trend data
  432. TopURLs []URLStats `json:"top_urls"` // TOP 10 URLs
  433. Browsers []BrowserStats `json:"browsers"` // Browser statistics
  434. OperatingSystems []OSStats `json:"operating_systems"` // OS statistics
  435. Devices []DeviceStats `json:"devices"` // Device statistics
  436. Summary struct {
  437. TotalUV int `json:"total_uv"` // Total unique visitors
  438. TotalPV int `json:"total_pv"` // Total page views
  439. AvgDailyUV float64 `json:"avg_daily_uv"` // Average daily UV
  440. AvgDailyPV float64 `json:"avg_daily_pv"` // Average daily PV
  441. PeakHour int `json:"peak_hour"` // Peak traffic hour (0-23)
  442. PeakHourTraffic int `json:"peak_hour_traffic"` // Peak hour PV count
  443. } `json:"summary"`
  444. }
  445. // GetDashboardAnalytics provides comprehensive dashboard analytics from modern analytics service
  446. func GetDashboardAnalytics(c *gin.Context) {
  447. var req DashboardRequest
  448. // Parse JSON body for POST request
  449. if err := c.ShouldBindJSON(&req); err != nil {
  450. cosy.ErrHandler(c, err)
  451. return
  452. }
  453. logger.Debugf("Dashboard API received log_path: '%s', start_date: '%s', end_date: '%s'", req.LogPath, req.StartDate, req.EndDate)
  454. analyticsService := nginx_log.GetAnalytics()
  455. if analyticsService == nil {
  456. cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
  457. return
  458. }
  459. // Use default access log path if LogPath is empty
  460. if req.LogPath == "" {
  461. defaultLogPath := nginx.GetAccessLogPath()
  462. if defaultLogPath != "" {
  463. req.LogPath = defaultLogPath
  464. logger.Debugf("Using default access log path: %s", req.LogPath)
  465. }
  466. }
  467. // Validate log path if provided
  468. if req.LogPath != "" {
  469. if err := analyticsService.ValidateLogPath(req.LogPath); err != nil {
  470. cosy.ErrHandler(c, err)
  471. return
  472. }
  473. }
  474. // Parse and validate date strings
  475. var startTime, endTime time.Time
  476. var err error
  477. if req.StartDate != "" {
  478. startTime, err = time.Parse("2006-01-02", req.StartDate)
  479. if err != nil {
  480. c.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid start_date format, expected YYYY-MM-DD: " + err.Error()})
  481. return
  482. }
  483. // Convert to UTC for consistent processing
  484. startTime = startTime.UTC()
  485. }
  486. if req.EndDate != "" {
  487. endTime, err = time.Parse("2006-01-02", req.EndDate)
  488. if err != nil {
  489. c.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid end_date format, expected YYYY-MM-DD: " + err.Error()})
  490. return
  491. }
  492. // Set end time to end of day and convert to UTC
  493. endTime = endTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second).UTC()
  494. }
  495. // Set default time range if not provided (last 30 days)
  496. if startTime.IsZero() || endTime.IsZero() {
  497. endTime = time.Now()
  498. startTime = endTime.AddDate(0, 0, -30) // 30 days ago
  499. }
  500. // Get dashboard analytics with timeout
  501. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
  502. defer cancel()
  503. logger.Debugf("Dashboard request for log_path: %s, parsed start_time: %v, end_time: %v", req.LogPath, startTime, endTime)
  504. // Use main_log_path field for efficient log group queries instead of expanding file paths
  505. // This provides much better performance by using indexed field filtering
  506. logger.Debugf("Dashboard querying log group with main_log_path: %s", req.LogPath)
  507. // Build dashboard query request
  508. dashboardReq := &analytics.DashboardQueryRequest{
  509. LogPath: req.LogPath,
  510. LogPaths: []string{req.LogPath}, // Use single main log path
  511. StartTime: startTime.Unix(),
  512. EndTime: endTime.Unix(),
  513. }
  514. logger.Debugf("Query parameters - LogPath='%s', StartTime=%v, EndTime=%v",
  515. dashboardReq.LogPath, dashboardReq.StartTime, dashboardReq.EndTime)
  516. // Get analytics from modern analytics service
  517. result, err := analyticsService.GetDashboardAnalytics(ctx, dashboardReq)
  518. if err != nil {
  519. cosy.ErrHandler(c, err)
  520. return
  521. }
  522. logger.Debugf("Successfully retrieved dashboard analytics")
  523. // Debug: Log summary of results
  524. if result != nil {
  525. logger.Debugf("Results summary - TotalUV=%d, TotalPV=%d, HourlyStats=%d, DailyStats=%d, TopURLs=%d",
  526. result.Summary.TotalUV, result.Summary.TotalPV,
  527. len(result.HourlyStats), len(result.DailyStats), len(result.TopURLs))
  528. } else {
  529. logger.Debugf("Analytics result is nil")
  530. }
  531. c.JSON(http.StatusOK, result)
  532. }
  533. // GetWorldMapData provides geographic data for world map visualization
  534. func GetWorldMapData(c *gin.Context) {
  535. var req AnalyticsRequest
  536. if err := c.ShouldBindJSON(&req); err != nil {
  537. cosy.ErrHandler(c, err)
  538. return
  539. }
  540. logger.Debugf("=== DEBUG GetWorldMapData START ===")
  541. logger.Debugf("WorldMapData request - Path: '%s', StartTime: %d, EndTime: %d, Limit: %d",
  542. req.Path, req.StartTime, req.EndTime, req.Limit)
  543. analyticsService := nginx_log.GetAnalytics()
  544. if analyticsService == nil {
  545. cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
  546. return
  547. }
  548. // Use default access log path if Path is empty
  549. if req.Path == "" {
  550. defaultLogPath := nginx.GetAccessLogPath()
  551. if defaultLogPath != "" {
  552. req.Path = defaultLogPath
  553. logger.Debugf("Using default access log path for world map: %s", req.Path)
  554. }
  555. }
  556. // Validate log path if provided
  557. if req.Path != "" {
  558. if err := analyticsService.ValidateLogPath(req.Path); err != nil {
  559. cosy.ErrHandler(c, err)
  560. return
  561. }
  562. }
  563. // Use main_log_path field for efficient log group queries instead of expanding file paths
  564. logger.Debugf("WorldMapData - Using main_log_path field for log group: %s", req.Path)
  565. // Get world map data with timeout
  566. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
  567. defer cancel()
  568. geoReq := &analytics.GeoQueryRequest{
  569. StartTime: req.StartTime,
  570. EndTime: req.EndTime,
  571. LogPath: req.Path,
  572. LogPaths: []string{req.Path}, // Use single main log path
  573. UseMainLogPath: true, // Use main_log_path field for efficient queries
  574. Limit: req.Limit,
  575. }
  576. logger.Debugf("WorldMapData - GeoQueryRequest: %+v", geoReq)
  577. data, err := analyticsService.GetGeoDistribution(ctx, geoReq)
  578. if err != nil {
  579. cosy.ErrHandler(c, err)
  580. return
  581. }
  582. logger.Debugf("WorldMapData - GetGeoDistribution returned data with %d countries", len(data.Countries))
  583. for code, count := range data.Countries {
  584. if code == "CN" {
  585. logger.Debugf("WorldMapData - CN country count: %d", count)
  586. }
  587. logger.Debugf("WorldMapData - Country: '%s', Count: %d", code, count)
  588. }
  589. // Transform map to slice for frontend chart compatibility, calculate percentages, and sort.
  590. chartData := make([]GeoRegionItem, 0, len(data.Countries))
  591. totalValue := 0
  592. for _, value := range data.Countries {
  593. totalValue += value
  594. }
  595. logger.Debugf("WorldMapData - Total value calculated: %d", totalValue)
  596. for code, value := range data.Countries {
  597. percent := 0.0
  598. if totalValue > 0 {
  599. percent = (float64(value) / float64(totalValue)) * 100
  600. }
  601. chartData = append(chartData, GeoRegionItem{Code: code, Value: value, Percent: percent})
  602. }
  603. // Sort by value descending
  604. sort.Slice(chartData, func(i, j int) bool {
  605. return chartData[i].Value > chartData[j].Value
  606. })
  607. logger.Debugf("WorldMapData - Final response data contains %d items with total value %d", len(chartData), totalValue)
  608. for i, item := range chartData {
  609. if item.Code == "CN" {
  610. logger.Debugf("WorldMapData - FOUND CN - [%d] Code: '%s', Value: %d, Percent: %.2f%%", i, item.Code, item.Value, item.Percent)
  611. }
  612. logger.Debugf("WorldMapData - [%d] Code: '%s', Value: %d, Percent: %.2f%%", i, item.Code, item.Value, item.Percent)
  613. }
  614. logger.Debugf("=== DEBUG GetWorldMapData END ===")
  615. c.JSON(http.StatusOK, GeoRegionResponse{
  616. Data: chartData,
  617. })
  618. }
  619. // GetChinaMapData provides geographic data for China map visualization
  620. func GetChinaMapData(c *gin.Context) {
  621. var req AnalyticsRequest
  622. if err := c.ShouldBindJSON(&req); err != nil {
  623. cosy.ErrHandler(c, err)
  624. return
  625. }
  626. logger.Debugf("=== DEBUG GetChinaMapData START ===")
  627. logger.Debugf("ChinaMapData request - Path: '%s', StartTime: %d, EndTime: %d, Limit: %d",
  628. req.Path, req.StartTime, req.EndTime, req.Limit)
  629. analyticsService := nginx_log.GetAnalytics()
  630. if analyticsService == nil {
  631. cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
  632. return
  633. }
  634. // Use default access log path if Path is empty
  635. if req.Path == "" {
  636. defaultLogPath := nginx.GetAccessLogPath()
  637. if defaultLogPath != "" {
  638. req.Path = defaultLogPath
  639. logger.Debugf("Using default access log path for China map: %s", req.Path)
  640. }
  641. }
  642. // Validate log path if provided
  643. if req.Path != "" {
  644. if err := analyticsService.ValidateLogPath(req.Path); err != nil {
  645. cosy.ErrHandler(c, err)
  646. return
  647. }
  648. }
  649. // Use main_log_path field for efficient log group queries instead of expanding file paths
  650. logger.Debugf("ChinaMapData - Using main_log_path field for log group: %s", req.Path)
  651. // Get China map data with timeout
  652. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
  653. defer cancel()
  654. geoReq := &analytics.GeoQueryRequest{
  655. StartTime: req.StartTime,
  656. EndTime: req.EndTime,
  657. LogPath: req.Path,
  658. LogPaths: []string{req.Path}, // Use single main log path
  659. UseMainLogPath: true, // Use main_log_path field for efficient queries
  660. Limit: req.Limit,
  661. }
  662. logger.Debugf("ChinaMapData - GeoQueryRequest: %+v", geoReq)
  663. // Get distribution specifically for China (country code "CN")
  664. logger.Debugf("ChinaMapData - About to call GetGeoDistributionByCountry with country code 'CN'")
  665. data, err := analyticsService.GetGeoDistributionByCountry(ctx, geoReq, "CN")
  666. if err != nil {
  667. logger.Debugf("ChinaMapData - GetGeoDistributionByCountry returned error: %v", err)
  668. cosy.ErrHandler(c, err)
  669. return
  670. }
  671. logger.Debugf("ChinaMapData - GetGeoDistributionByCountry returned data with %d provinces", len(data.Countries))
  672. for name, count := range data.Countries {
  673. logger.Debugf("ChinaMapData - Province: '%s', Count: %d", name, count)
  674. }
  675. // Transform map to slice for frontend chart compatibility, calculate percentages, and sort.
  676. chartData := make([]GeoDataItem, 0, len(data.Countries))
  677. totalValue := 0
  678. for _, value := range data.Countries {
  679. totalValue += value
  680. }
  681. logger.Debugf("ChinaMapData - Total value calculated: %d", totalValue)
  682. for name, value := range data.Countries {
  683. percent := 0.0
  684. if totalValue > 0 {
  685. percent = (float64(value) / float64(totalValue)) * 100
  686. }
  687. chartData = append(chartData, GeoDataItem{Name: name, Value: value, Percent: percent})
  688. }
  689. // Sort by value descending
  690. sort.Slice(chartData, func(i, j int) bool {
  691. return chartData[i].Value > chartData[j].Value
  692. })
  693. logger.Debugf("ChinaMapData - Final response data contains %d items with total value %d", len(chartData), totalValue)
  694. for i, item := range chartData {
  695. logger.Debugf("ChinaMapData - [%d] Name: '%s', Value: %d, Percent: %.2f%%", i, item.Name, item.Value, item.Percent)
  696. }
  697. logger.Debugf("=== DEBUG GetChinaMapData END ===")
  698. c.JSON(http.StatusOK, GeoDataResponse{
  699. Data: chartData,
  700. })
  701. }
  702. // GetGeoStats provides geographic statistics
  703. func GetGeoStats(c *gin.Context) {
  704. var req AnalyticsRequest
  705. if err := c.ShouldBindJSON(&req); err != nil {
  706. c.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid JSON request body: " + err.Error()})
  707. return
  708. }
  709. analyticsService := nginx_log.GetAnalytics()
  710. if analyticsService == nil {
  711. cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
  712. return
  713. }
  714. // Use default access log path if Path is empty
  715. if req.Path == "" {
  716. defaultLogPath := nginx.GetAccessLogPath()
  717. if defaultLogPath != "" {
  718. req.Path = defaultLogPath
  719. logger.Debugf("Using default access log path for geo stats: %s", req.Path)
  720. }
  721. }
  722. // Validate log path if provided
  723. if req.Path != "" {
  724. if err := analyticsService.ValidateLogPath(req.Path); err != nil {
  725. cosy.ErrHandler(c, err)
  726. return
  727. }
  728. }
  729. // Use main_log_path field for efficient log group queries instead of expanding file paths
  730. logger.Debugf("GeoStats - Using main_log_path field for log group: %s", req.Path)
  731. // Set default limit if not provided
  732. if req.Limit == 0 {
  733. req.Limit = 20
  734. }
  735. // Get geographic statistics with timeout
  736. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
  737. defer cancel()
  738. geoReq := &analytics.GeoQueryRequest{
  739. StartTime: req.StartTime,
  740. EndTime: req.EndTime,
  741. LogPath: req.Path,
  742. LogPaths: []string{req.Path}, // Use single main log path
  743. UseMainLogPath: true, // Use main_log_path field for efficient queries
  744. Limit: req.Limit,
  745. }
  746. stats, err := analyticsService.GetTopCountries(ctx, geoReq)
  747. if err != nil {
  748. cosy.ErrHandler(c, err)
  749. return
  750. }
  751. // Convert to []interface{} for JSON serialization
  752. statsInterface := make([]interface{}, len(stats))
  753. for i, stat := range stats {
  754. statsInterface[i] = stat
  755. }
  756. c.JSON(http.StatusOK, GeoStatsResponse{
  757. Stats: statsInterface,
  758. })
  759. }
  760. // getCardinalityCount is a helper function to get accurate cardinality counts using the analytics service
  761. func getCardinalityCount(ctx context.Context, field string, searchReq *searcher.SearchRequest) int {
  762. logger.Debugf("🔍 getCardinalityCount: Starting cardinality count for field '%s'", field)
  763. // Create a CardinalityRequest from the SearchRequest
  764. cardReq := &searcher.CardinalityRequest{
  765. Field: field,
  766. StartTime: searchReq.StartTime,
  767. EndTime: searchReq.EndTime,
  768. LogPaths: searchReq.LogPaths,
  769. UseMainLogPath: searchReq.UseMainLogPath, // Use main_log_path field if enabled
  770. }
  771. logger.Debugf("🔍 CardinalityRequest: Field=%s, StartTime=%v, EndTime=%v, LogPaths=%v",
  772. cardReq.Field, cardReq.StartTime, cardReq.EndTime, cardReq.LogPaths)
  773. // Try to get the searcher to access cardinality counter
  774. searcherService := nginx_log.GetSearcher()
  775. if searcherService == nil {
  776. logger.Debugf("🚨 getCardinalityCount: ModernSearcher not available for field %s", field)
  777. return 0
  778. }
  779. logger.Debugf("🔍 getCardinalityCount: ModernSearcher available, type: %T", searcherService)
  780. // Use searcher to access Counter
  781. if searcherService != nil {
  782. ds := searcherService
  783. logger.Debugf("🔍 getCardinalityCount: Successfully cast to Searcher")
  784. shards := ds.GetShards()
  785. logger.Debugf("🔍 getCardinalityCount: Retrieved %d shards", len(shards))
  786. if len(shards) > 0 {
  787. // Check shard health
  788. for i, shard := range shards {
  789. logger.Debugf("🔍 getCardinalityCount: Shard %d: %v", i, shard != nil)
  790. }
  791. cardinalityCounter := searcher.NewCounter(shards)
  792. logger.Debugf("🔍 getCardinalityCount: Created Counter")
  793. result, err := cardinalityCounter.Count(ctx, cardReq)
  794. if err != nil {
  795. logger.Debugf("🚨 getCardinalityCount: Counter failed for field %s: %v", field, err)
  796. return 0
  797. }
  798. if result.Error != "" {
  799. logger.Debugf("🚨 getCardinalityCount: Counter returned error for field %s: %s", field, result.Error)
  800. return 0
  801. }
  802. logger.Debugf("✅ getCardinalityCount: Successfully got cardinality for field %s: %d", field, result.Cardinality)
  803. return int(result.Cardinality)
  804. } else {
  805. logger.Debugf("🚨 getCardinalityCount: Searcher has no shards for field %s", field)
  806. }
  807. } else {
  808. logger.Debugf("🚨 getCardinalityCount: Searcher is not Searcher (type: %T) for field %s", searcherService, field)
  809. }
  810. return 0
  811. }