analytics.go 27 KB

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