analytics.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971
  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. Timeout: 30 * time.Second, // Add timeout for large facet operations
  218. IncludeHighlighting: true,
  219. IncludeFacets: true, // Re-enable facets for accurate summary stats
  220. FacetFields: []string{"ip", "path_exact"}, // For UV and Unique Pages
  221. FacetSize: 10000, // Balanced: large enough for most cases, but not excessive
  222. }
  223. // If no sorting is specified, default to sorting by timestamp descending.
  224. if searchReq.SortBy == "" {
  225. searchReq.SortBy = "timestamp"
  226. searchReq.SortOrder = "desc"
  227. }
  228. // Expand the base log path to all physical files in the group using filesystem globbing.
  229. if req.LogPath != "" {
  230. logPaths, err := nginx_log.ExpandLogGroupPath(req.LogPath)
  231. if err != nil {
  232. logger.Warnf("Could not expand log group path %s: %v", req.LogPath, err)
  233. // Fallback to using the raw path
  234. searchReq.LogPaths = []string{req.LogPath}
  235. } else {
  236. searchReq.LogPaths = logPaths
  237. }
  238. }
  239. // Add time filters
  240. if req.StartTime > 0 {
  241. searchReq.StartTime = &req.StartTime
  242. }
  243. if req.EndTime > 0 {
  244. searchReq.EndTime = &req.EndTime
  245. }
  246. // If no time range is provided, default to searching all time.
  247. if searchReq.StartTime == nil && searchReq.EndTime == nil {
  248. var startTime int64 = 0 // Unix epoch
  249. now := time.Now().Unix()
  250. searchReq.StartTime = &startTime
  251. searchReq.EndTime = &now
  252. }
  253. // Add field filters
  254. if req.IP != "" {
  255. searchReq.IPAddresses = []string{req.IP}
  256. }
  257. if req.Method != "" {
  258. searchReq.Methods = []string{req.Method}
  259. }
  260. if req.Path != "" {
  261. searchReq.Paths = []string{req.Path}
  262. }
  263. if req.UserAgent != "" {
  264. searchReq.UserAgents = []string{req.UserAgent}
  265. }
  266. if req.Referer != "" {
  267. searchReq.Referers = []string{req.Referer}
  268. }
  269. if req.Browser != "" {
  270. searchReq.Browsers = []string{req.Browser}
  271. }
  272. if req.OS != "" {
  273. searchReq.OSs = []string{req.OS}
  274. }
  275. if req.Device != "" {
  276. searchReq.Devices = []string{req.Device}
  277. }
  278. if len(req.Status) > 0 {
  279. searchReq.StatusCodes = req.Status
  280. }
  281. // Execute search with timeout
  282. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Minute)
  283. defer cancel()
  284. result, err := searcherService.Search(ctx, searchReq)
  285. if err != nil {
  286. cosy.ErrHandler(c, err)
  287. return
  288. }
  289. // --- Transform the searcher result to the API response structure ---
  290. // 1. Extract entries from hits
  291. entries := make([]map[string]interface{}, len(result.Hits))
  292. var totalTraffic int64 // Total traffic is for the entire result set, must be calculated separately if needed.
  293. for i, hit := range result.Hits {
  294. entries[i] = hit.Fields
  295. if bytesSent, ok := hit.Fields["bytes_sent"].(float64); ok {
  296. totalTraffic += int64(bytesSent)
  297. }
  298. }
  299. // 2. Calculate summary stats from the overall results using CardinalityCounter for accuracy
  300. pv := int(result.TotalHits)
  301. var uv, uniquePages int
  302. var facetUV, facetUniquePages int
  303. // First get facet values as fallback
  304. if result.Facets != nil {
  305. if ipFacet, ok := result.Facets["ip"]; ok {
  306. facetUV = ipFacet.Total // .Total on a facet gives the count of unique terms
  307. uv = facetUV
  308. }
  309. if pathFacet, ok := result.Facets["path_exact"]; ok {
  310. facetUniquePages = pathFacet.Total
  311. uniquePages = facetUniquePages
  312. }
  313. }
  314. // Override with CardinalityCounter results for better accuracy
  315. if analyticsService != nil {
  316. // Get cardinality counts for UV (unique IPs)
  317. if uvResult := getCardinalityCount(ctx, analyticsService, "ip", searchReq); uvResult > 0 {
  318. uv = uvResult
  319. logger.Debugf("🔢 Search endpoint - UV from CardinalityCounter: %d (vs facet: %d)", uvResult, facetUV)
  320. }
  321. // Get cardinality counts for Unique Pages (unique paths)
  322. if upResult := getCardinalityCount(ctx, analyticsService, "path_exact", searchReq); upResult > 0 {
  323. uniquePages = upResult
  324. logger.Debugf("🔢 Search endpoint - Unique Pages from CardinalityCounter: %d (vs facet: %d)", upResult, facetUniquePages)
  325. }
  326. }
  327. // Note: TotalTraffic is not available for the whole result set without a separate query.
  328. // We will approximate it based on the current page's average for now.
  329. var avgBytesOnPage float64
  330. if len(result.Hits) > 0 {
  331. avgBytesOnPage = float64(totalTraffic) / float64(len(result.Hits))
  332. }
  333. approximatedTotalTraffic := int64(avgBytesOnPage * float64(pv))
  334. var avgTraffic float64
  335. if pv > 0 {
  336. avgTraffic = float64(approximatedTotalTraffic) / float64(pv)
  337. }
  338. summary := SummaryStats{
  339. UV: uv,
  340. PV: pv,
  341. TotalTraffic: approximatedTotalTraffic,
  342. UniquePages: uniquePages,
  343. AvgTrafficPerPV: avgTraffic,
  344. }
  345. // 3. Assemble the final response
  346. apiResponse := AdvancedSearchResponseAPI{
  347. Entries: entries,
  348. Total: result.TotalHits,
  349. Took: result.Duration.Milliseconds(),
  350. Query: req.Query,
  351. Summary: summary,
  352. }
  353. c.JSON(http.StatusOK, apiResponse)
  354. }
  355. // GetLogEntries provides simple log entry retrieval
  356. func GetLogEntries(c *gin.Context) {
  357. var req struct {
  358. Path string `json:"path" form:"path"`
  359. Limit int `json:"limit" form:"limit"`
  360. Tail bool `json:"tail" form:"tail"` // Get latest entries
  361. }
  362. if !cosy.BindAndValid(c, &req) {
  363. return
  364. }
  365. searcherService := nginx_log.GetModernSearcher()
  366. if searcherService == nil {
  367. cosy.ErrHandler(c, nginx_log.ErrModernSearcherNotAvailable)
  368. return
  369. }
  370. analyticsService := nginx_log.GetModernAnalytics()
  371. if analyticsService == nil {
  372. cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
  373. return
  374. }
  375. // Validate log path
  376. if err := analyticsService.ValidateLogPath(req.Path); err != nil {
  377. cosy.ErrHandler(c, err)
  378. return
  379. }
  380. // Set default limit
  381. if req.Limit == 0 {
  382. req.Limit = 100
  383. }
  384. // Build search request
  385. searchReq := &searcher.SearchRequest{
  386. Limit: req.Limit,
  387. UseCache: false, // Don't cache simple entry requests
  388. SortBy: "timestamp",
  389. SortOrder: "desc", // Latest first by default
  390. }
  391. if req.Tail {
  392. searchReq.SortOrder = "desc" // Latest entries first
  393. } else {
  394. searchReq.SortOrder = "asc" // Oldest entries first
  395. }
  396. // Execute search
  397. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
  398. defer cancel()
  399. result, err := searcherService.Search(ctx, searchReq)
  400. if err != nil {
  401. cosy.ErrHandler(c, err)
  402. return
  403. }
  404. // Convert search hits to simple entries format
  405. var entries []map[string]interface{}
  406. for _, hit := range result.Hits {
  407. entries = append(entries, hit.Fields)
  408. }
  409. c.JSON(http.StatusOK, gin.H{
  410. "entries": entries,
  411. "count": len(entries),
  412. })
  413. }
  414. // DashboardRequest represents the request for dashboard analytics
  415. type DashboardRequest struct {
  416. LogPath string `json:"log_path" form:"log_path"`
  417. StartDate string `json:"start_date" form:"start_date"` // Format: 2006-01-02
  418. EndDate string `json:"end_date" form:"end_date"` // Format: 2006-01-02
  419. }
  420. // HourlyStats represents hourly UV/PV statistics
  421. type HourlyStats struct {
  422. Hour int `json:"hour"` // 0-23
  423. UV int `json:"uv"` // Unique visitors (unique IPs)
  424. PV int `json:"pv"` // Page views (total requests)
  425. Timestamp int64 `json:"timestamp"` // Unix timestamp for the hour
  426. }
  427. // DailyStats represents daily access statistics
  428. type DailyStats struct {
  429. Date string `json:"date"` // YYYY-MM-DD format
  430. UV int `json:"uv"` // Unique visitors
  431. PV int `json:"pv"` // Page views
  432. Timestamp int64 `json:"timestamp"` // Unix timestamp for the day
  433. }
  434. // URLStats represents URL access statistics
  435. type URLStats struct {
  436. URL string `json:"url"`
  437. Visits int `json:"visits"`
  438. Percent float64 `json:"percent"`
  439. }
  440. // BrowserStats represents browser statistics
  441. type BrowserStats struct {
  442. Browser string `json:"browser"`
  443. Count int `json:"count"`
  444. Percent float64 `json:"percent"`
  445. }
  446. // OSStats represents operating system statistics
  447. type OSStats struct {
  448. OS string `json:"os"`
  449. Count int `json:"count"`
  450. Percent float64 `json:"percent"`
  451. }
  452. // DeviceStats represents device type statistics
  453. type DeviceStats struct {
  454. Device string `json:"device"`
  455. Count int `json:"count"`
  456. Percent float64 `json:"percent"`
  457. }
  458. // DashboardResponse represents the dashboard analytics response
  459. type DashboardResponse struct {
  460. HourlyStats []HourlyStats `json:"hourly_stats"` // 24-hour UV/PV data
  461. DailyStats []DailyStats `json:"daily_stats"` // Monthly trend data
  462. TopURLs []URLStats `json:"top_urls"` // TOP 10 URLs
  463. Browsers []BrowserStats `json:"browsers"` // Browser statistics
  464. OperatingSystems []OSStats `json:"operating_systems"` // OS statistics
  465. Devices []DeviceStats `json:"devices"` // Device statistics
  466. Summary struct {
  467. TotalUV int `json:"total_uv"` // Total unique visitors
  468. TotalPV int `json:"total_pv"` // Total page views
  469. AvgDailyUV float64 `json:"avg_daily_uv"` // Average daily UV
  470. AvgDailyPV float64 `json:"avg_daily_pv"` // Average daily PV
  471. PeakHour int `json:"peak_hour"` // Peak traffic hour (0-23)
  472. PeakHourTraffic int `json:"peak_hour_traffic"` // Peak hour PV count
  473. } `json:"summary"`
  474. }
  475. // GetDashboardAnalytics provides comprehensive dashboard analytics from modern analytics service
  476. func GetDashboardAnalytics(c *gin.Context) {
  477. var req DashboardRequest
  478. // Parse JSON body for POST request
  479. if err := c.ShouldBindJSON(&req); err != nil {
  480. cosy.ErrHandler(c, err)
  481. return
  482. }
  483. logger.Debugf("Dashboard API received log_path: '%s', start_date: '%s', end_date: '%s'", req.LogPath, req.StartDate, req.EndDate)
  484. analyticsService := nginx_log.GetModernAnalytics()
  485. if analyticsService == nil {
  486. cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
  487. return
  488. }
  489. // Use default access log path if LogPath is empty
  490. if req.LogPath == "" {
  491. defaultLogPath := nginx.GetAccessLogPath()
  492. if defaultLogPath != "" {
  493. req.LogPath = defaultLogPath
  494. logger.Debugf("Using default access log path: %s", req.LogPath)
  495. }
  496. }
  497. // Validate log path if provided
  498. if req.LogPath != "" {
  499. if err := analyticsService.ValidateLogPath(req.LogPath); err != nil {
  500. cosy.ErrHandler(c, err)
  501. return
  502. }
  503. }
  504. // Parse and validate date strings
  505. var startTime, endTime time.Time
  506. var err error
  507. if req.StartDate != "" {
  508. startTime, err = time.Parse("2006-01-02", req.StartDate)
  509. if err != nil {
  510. c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid start_date format, expected YYYY-MM-DD: " + err.Error()})
  511. return
  512. }
  513. }
  514. if req.EndDate != "" {
  515. endTime, err = time.Parse("2006-01-02", req.EndDate)
  516. if err != nil {
  517. c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid end_date format, expected YYYY-MM-DD: " + err.Error()})
  518. return
  519. }
  520. // Set end time to end of day
  521. endTime = endTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
  522. }
  523. // Set default time range if not provided (last 30 days)
  524. if startTime.IsZero() || endTime.IsZero() {
  525. endTime = time.Now()
  526. startTime = endTime.AddDate(0, 0, -30) // 30 days ago
  527. }
  528. // Get dashboard analytics with timeout
  529. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
  530. defer cancel()
  531. logger.Debugf("Dashboard request for log_path: %s, parsed start_time: %v, end_time: %v", req.LogPath, startTime, endTime)
  532. // Expand the log path to its full list of physical files
  533. logPaths, err := nginx_log.ExpandLogGroupPath(req.LogPath)
  534. if err != nil {
  535. // Log the error but proceed with the base path as a fallback
  536. logger.Warnf("Could not expand log group path for dashboard %s: %v", req.LogPath, err)
  537. logPaths = []string{req.LogPath}
  538. }
  539. // Build dashboard query request
  540. dashboardReq := &analytics.DashboardQueryRequest{
  541. LogPath: req.LogPath,
  542. LogPaths: logPaths,
  543. StartTime: startTime.Unix(),
  544. EndTime: endTime.Unix(),
  545. }
  546. logger.Debugf("Query parameters - LogPath='%s', StartTime=%v, EndTime=%v",
  547. dashboardReq.LogPath, dashboardReq.StartTime, dashboardReq.EndTime)
  548. // Get analytics from modern analytics service
  549. result, err := analyticsService.GetDashboardAnalytics(ctx, dashboardReq)
  550. if err != nil {
  551. cosy.ErrHandler(c, err)
  552. return
  553. }
  554. logger.Debugf("Successfully retrieved dashboard analytics")
  555. // Debug: Log summary of results
  556. if result != nil {
  557. logger.Debugf("Results summary - TotalUV=%d, TotalPV=%d, HourlyStats=%d, DailyStats=%d, TopURLs=%d",
  558. result.Summary.TotalUV, result.Summary.TotalPV,
  559. len(result.HourlyStats), len(result.DailyStats), len(result.TopURLs))
  560. } else {
  561. logger.Debugf("Analytics result is nil")
  562. }
  563. c.JSON(http.StatusOK, result)
  564. }
  565. // GetWorldMapData provides geographic data for world map visualization
  566. func GetWorldMapData(c *gin.Context) {
  567. var req AnalyticsRequest
  568. if err := c.ShouldBindJSON(&req); err != nil {
  569. cosy.ErrHandler(c, err)
  570. return
  571. }
  572. logger.Debugf("=== DEBUG GetWorldMapData START ===")
  573. logger.Debugf("WorldMapData request - Path: '%s', StartTime: %d, EndTime: %d, Limit: %d",
  574. req.Path, req.StartTime, req.EndTime, req.Limit)
  575. analyticsService := nginx_log.GetModernAnalytics()
  576. if analyticsService == nil {
  577. cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
  578. return
  579. }
  580. // Use default access log path if Path is empty
  581. if req.Path == "" {
  582. defaultLogPath := nginx.GetAccessLogPath()
  583. if defaultLogPath != "" {
  584. req.Path = defaultLogPath
  585. logger.Debugf("Using default access log path for world map: %s", req.Path)
  586. }
  587. }
  588. // Validate log path if provided
  589. if req.Path != "" {
  590. if err := analyticsService.ValidateLogPath(req.Path); err != nil {
  591. cosy.ErrHandler(c, err)
  592. return
  593. }
  594. }
  595. // Expand log path for filtering
  596. logPaths, err := nginx_log.ExpandLogGroupPath(req.Path)
  597. if err != nil {
  598. logger.Warnf("Could not expand log group path for world map %s: %v", req.Path, err)
  599. logPaths = []string{req.Path} // Fallback
  600. }
  601. logger.Debugf("WorldMapData - Expanded log paths: %v", logPaths)
  602. // Get world map data with timeout
  603. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
  604. defer cancel()
  605. geoReq := &analytics.GeoQueryRequest{
  606. StartTime: req.StartTime,
  607. EndTime: req.EndTime,
  608. LogPath: req.Path,
  609. LogPaths: logPaths,
  610. Limit: req.Limit,
  611. }
  612. logger.Debugf("WorldMapData - GeoQueryRequest: %+v", geoReq)
  613. data, err := analyticsService.GetGeoDistribution(ctx, geoReq)
  614. if err != nil {
  615. cosy.ErrHandler(c, err)
  616. return
  617. }
  618. logger.Debugf("WorldMapData - GetGeoDistribution returned data with %d countries", len(data.Countries))
  619. for code, count := range data.Countries {
  620. if code == "CN" {
  621. logger.Debugf("WorldMapData - CN country count: %d", count)
  622. }
  623. logger.Debugf("WorldMapData - Country: '%s', Count: %d", code, count)
  624. }
  625. // Transform map to slice for frontend chart compatibility, calculate percentages, and sort.
  626. chartData := make([]GeoRegionItem, 0, len(data.Countries))
  627. totalValue := 0
  628. for _, value := range data.Countries {
  629. totalValue += value
  630. }
  631. logger.Debugf("WorldMapData - Total value calculated: %d", totalValue)
  632. for code, value := range data.Countries {
  633. percent := 0.0
  634. if totalValue > 0 {
  635. percent = (float64(value) / float64(totalValue)) * 100
  636. }
  637. chartData = append(chartData, GeoRegionItem{Code: code, Value: value, Percent: percent})
  638. }
  639. // Sort by value descending
  640. sort.Slice(chartData, func(i, j int) bool {
  641. return chartData[i].Value > chartData[j].Value
  642. })
  643. logger.Debugf("WorldMapData - Final response data contains %d items with total value %d", len(chartData), totalValue)
  644. for i, item := range chartData {
  645. if item.Code == "CN" {
  646. logger.Debugf("WorldMapData - FOUND CN - [%d] Code: '%s', Value: %d, Percent: %.2f%%", i, item.Code, item.Value, item.Percent)
  647. }
  648. logger.Debugf("WorldMapData - [%d] Code: '%s', Value: %d, Percent: %.2f%%", i, item.Code, item.Value, item.Percent)
  649. }
  650. logger.Debugf("=== DEBUG GetWorldMapData END ===")
  651. c.JSON(http.StatusOK, gin.H{
  652. "data": chartData,
  653. })
  654. }
  655. // GetChinaMapData provides geographic data for China map visualization
  656. func GetChinaMapData(c *gin.Context) {
  657. var req AnalyticsRequest
  658. if err := c.ShouldBindJSON(&req); err != nil {
  659. cosy.ErrHandler(c, err)
  660. return
  661. }
  662. logger.Debugf("=== DEBUG GetChinaMapData START ===")
  663. logger.Debugf("ChinaMapData request - Path: '%s', StartTime: %d, EndTime: %d, Limit: %d",
  664. req.Path, req.StartTime, req.EndTime, req.Limit)
  665. analyticsService := nginx_log.GetModernAnalytics()
  666. if analyticsService == nil {
  667. cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
  668. return
  669. }
  670. // Use default access log path if Path is empty
  671. if req.Path == "" {
  672. defaultLogPath := nginx.GetAccessLogPath()
  673. if defaultLogPath != "" {
  674. req.Path = defaultLogPath
  675. logger.Debugf("Using default access log path for China map: %s", req.Path)
  676. }
  677. }
  678. // Validate log path if provided
  679. if req.Path != "" {
  680. if err := analyticsService.ValidateLogPath(req.Path); err != nil {
  681. cosy.ErrHandler(c, err)
  682. return
  683. }
  684. }
  685. // Expand log path for filtering
  686. logPaths, err := nginx_log.ExpandLogGroupPath(req.Path)
  687. if err != nil {
  688. logger.Warnf("Could not expand log group path for China map %s: %v", req.Path, err)
  689. logPaths = []string{req.Path} // Fallback
  690. }
  691. logger.Debugf("ChinaMapData - Expanded log paths: %v", logPaths)
  692. // Get China map data with timeout
  693. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
  694. defer cancel()
  695. geoReq := &analytics.GeoQueryRequest{
  696. StartTime: req.StartTime,
  697. EndTime: req.EndTime,
  698. LogPath: req.Path,
  699. LogPaths: logPaths,
  700. Limit: req.Limit,
  701. }
  702. logger.Debugf("ChinaMapData - GeoQueryRequest: %+v", geoReq)
  703. // Get distribution specifically for China (country code "CN")
  704. logger.Debugf("ChinaMapData - About to call GetGeoDistributionByCountry with country code 'CN'")
  705. data, err := analyticsService.GetGeoDistributionByCountry(ctx, geoReq, "CN")
  706. if err != nil {
  707. logger.Debugf("ChinaMapData - GetGeoDistributionByCountry returned error: %v", err)
  708. cosy.ErrHandler(c, err)
  709. return
  710. }
  711. logger.Debugf("ChinaMapData - GetGeoDistributionByCountry returned data with %d provinces", len(data.Countries))
  712. for name, count := range data.Countries {
  713. logger.Debugf("ChinaMapData - Province: '%s', Count: %d", name, count)
  714. }
  715. // Transform map to slice for frontend chart compatibility, calculate percentages, and sort.
  716. chartData := make([]GeoDataItem, 0, len(data.Countries))
  717. totalValue := 0
  718. for _, value := range data.Countries {
  719. totalValue += value
  720. }
  721. logger.Debugf("ChinaMapData - Total value calculated: %d", totalValue)
  722. for name, value := range data.Countries {
  723. percent := 0.0
  724. if totalValue > 0 {
  725. percent = (float64(value) / float64(totalValue)) * 100
  726. }
  727. chartData = append(chartData, GeoDataItem{Name: name, Value: value, Percent: percent})
  728. }
  729. // Sort by value descending
  730. sort.Slice(chartData, func(i, j int) bool {
  731. return chartData[i].Value > chartData[j].Value
  732. })
  733. logger.Debugf("ChinaMapData - Final response data contains %d items with total value %d", len(chartData), totalValue)
  734. for i, item := range chartData {
  735. logger.Debugf("ChinaMapData - [%d] Name: '%s', Value: %d, Percent: %.2f%%", i, item.Name, item.Value, item.Percent)
  736. }
  737. logger.Debugf("=== DEBUG GetChinaMapData END ===")
  738. c.JSON(http.StatusOK, gin.H{
  739. "data": chartData,
  740. })
  741. }
  742. // GetGeoStats provides geographic statistics
  743. func GetGeoStats(c *gin.Context) {
  744. var req AnalyticsRequest
  745. if err := c.ShouldBindJSON(&req); err != nil {
  746. c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON request body: " + err.Error()})
  747. return
  748. }
  749. analyticsService := nginx_log.GetModernAnalytics()
  750. if analyticsService == nil {
  751. cosy.ErrHandler(c, nginx_log.ErrModernAnalyticsNotAvailable)
  752. return
  753. }
  754. // Use default access log path if Path is empty
  755. if req.Path == "" {
  756. defaultLogPath := nginx.GetAccessLogPath()
  757. if defaultLogPath != "" {
  758. req.Path = defaultLogPath
  759. logger.Debugf("Using default access log path for geo stats: %s", req.Path)
  760. }
  761. }
  762. // Validate log path if provided
  763. if req.Path != "" {
  764. if err := analyticsService.ValidateLogPath(req.Path); err != nil {
  765. cosy.ErrHandler(c, err)
  766. return
  767. }
  768. }
  769. // Expand log path for filtering
  770. logPaths, err := nginx_log.ExpandLogGroupPath(req.Path)
  771. if err != nil {
  772. logger.Warnf("Could not expand log group path for geo stats %s: %v", req.Path, err)
  773. logPaths = []string{req.Path} // Fallback
  774. }
  775. // Set default limit if not provided
  776. if req.Limit == 0 {
  777. req.Limit = 20
  778. }
  779. // Get geographic statistics with timeout
  780. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
  781. defer cancel()
  782. geoReq := &analytics.GeoQueryRequest{
  783. StartTime: req.StartTime,
  784. EndTime: req.EndTime,
  785. LogPath: req.Path,
  786. LogPaths: logPaths,
  787. Limit: req.Limit,
  788. }
  789. stats, err := analyticsService.GetTopCountries(ctx, geoReq)
  790. if err != nil {
  791. cosy.ErrHandler(c, err)
  792. return
  793. }
  794. c.JSON(http.StatusOK, gin.H{
  795. "stats": stats,
  796. })
  797. }
  798. // getCardinalityCount is a helper function to get accurate cardinality counts using the analytics service
  799. func getCardinalityCount(ctx context.Context, analyticsService analytics.Service, field string, searchReq *searcher.SearchRequest) int {
  800. // Create a CardinalityRequest from the SearchRequest
  801. cardReq := &searcher.CardinalityRequest{
  802. Field: field,
  803. StartTime: searchReq.StartTime,
  804. EndTime: searchReq.EndTime,
  805. LogPaths: searchReq.LogPaths,
  806. }
  807. // Try to get the searcher to access cardinality counter
  808. searcherService := nginx_log.GetModernSearcher()
  809. if searcherService == nil {
  810. logger.Debugf("🚨 getCardinalityCount: ModernSearcher not available for field %s", field)
  811. return 0
  812. }
  813. // Try to cast to DistributedSearcher to access CardinalityCounter
  814. if ds, ok := searcherService.(*searcher.DistributedSearcher); ok {
  815. shards := ds.GetShards()
  816. if len(shards) > 0 {
  817. cardinalityCounter := searcher.NewCardinalityCounter(shards)
  818. result, err := cardinalityCounter.CountCardinality(ctx, cardReq)
  819. if err != nil {
  820. logger.Debugf("🚨 getCardinalityCount: CardinalityCounter failed for field %s: %v", field, err)
  821. return 0
  822. }
  823. if result.Error != "" {
  824. logger.Debugf("🚨 getCardinalityCount: CardinalityCounter returned error for field %s: %s", field, result.Error)
  825. return 0
  826. }
  827. logger.Debugf("✅ getCardinalityCount: Successfully got cardinality for field %s: %d", field, result.Cardinality)
  828. return int(result.Cardinality)
  829. } else {
  830. logger.Debugf("🚨 getCardinalityCount: DistributedSearcher has no shards for field %s", field)
  831. }
  832. } else {
  833. logger.Debugf("🚨 getCardinalityCount: Searcher is not DistributedSearcher (type: %T) for field %s", searcherService, field)
  834. }
  835. return 0
  836. }