analytics.go 30 KB

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