1
0

analytics.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. package nginx_log
  2. import (
  3. "context"
  4. "net/http"
  5. "time"
  6. "github.com/0xJacky/Nginx-UI/internal/nginx"
  7. "github.com/0xJacky/Nginx-UI/internal/nginx_log"
  8. "github.com/gin-gonic/gin"
  9. "github.com/uozi-tech/cosy"
  10. "github.com/uozi-tech/cosy/logger"
  11. )
  12. // AnalyticsRequest represents the request for log analytics
  13. type AnalyticsRequest struct {
  14. Path string `json:"path" form:"path"`
  15. StartTime int64 `json:"start_time" form:"start_time"`
  16. EndTime int64 `json:"end_time" form:"end_time"`
  17. Limit int `json:"limit" form:"limit"`
  18. }
  19. // AdvancedSearchRequest represents the request for advanced log search
  20. type AdvancedSearchRequest struct {
  21. Query string `json:"query" form:"query"`
  22. LogPath string `json:"log_path" form:"log_path"`
  23. StartTime int64 `json:"start_time" form:"start_time"`
  24. EndTime int64 `json:"end_time" form:"end_time"`
  25. IP string `json:"ip" form:"ip"`
  26. Method string `json:"method" form:"method"`
  27. Status []int `json:"status" form:"status"`
  28. Path string `json:"path" form:"path"`
  29. UserAgent string `json:"user_agent" form:"user_agent"`
  30. Referer string `json:"referer" form:"referer"`
  31. Browser string `json:"browser" form:"browser"`
  32. OS string `json:"os" form:"os"`
  33. Device string `json:"device" form:"device"`
  34. Limit int `json:"limit" form:"limit"`
  35. Offset int `json:"offset" form:"offset"`
  36. SortBy string `json:"sort_by" form:"sort_by"`
  37. SortOrder string `json:"sort_order" form:"sort_order"`
  38. }
  39. // PreflightResponse represents the response for preflight query
  40. type PreflightResponse struct {
  41. StartTime *int64 `json:"start_time,omitempty"`
  42. EndTime *int64 `json:"end_time,omitempty"`
  43. Available bool `json:"available"`
  44. IndexStatus string `json:"index_status"`
  45. }
  46. // GetLogAnalytics provides comprehensive log analytics
  47. func GetLogAnalytics(c *gin.Context) {
  48. var req AnalyticsRequest
  49. if !cosy.BindAndValid(c, &req) {
  50. return
  51. }
  52. // Get analytics service
  53. service := nginx_log.GetAnalyticsService()
  54. if service == nil {
  55. cosy.ErrHandler(c, nginx_log.ErrAnalyticsServiceNotAvailable)
  56. return
  57. }
  58. // Validate log path
  59. if err := service.ValidateLogPath(req.Path); err != nil {
  60. cosy.ErrHandler(c, err)
  61. return
  62. }
  63. // Analyze log file
  64. analytics, err := service.AnalyzeLogFile(req.Path)
  65. if err != nil {
  66. cosy.ErrHandler(c, err)
  67. return
  68. }
  69. c.JSON(http.StatusOK, analytics)
  70. }
  71. // GetLogPreflight returns the preflight status for log indexing
  72. func GetLogPreflight(c *gin.Context) {
  73. service := nginx_log.GetAnalyticsService()
  74. if service == nil {
  75. cosy.ErrHandler(c, nginx_log.ErrAnalyticsServiceNotAvailable)
  76. return
  77. }
  78. // Get optional log path parameter
  79. logPath := c.Query("log_path")
  80. // Use default access log path if logPath is empty
  81. if logPath == "" {
  82. defaultLogPath := nginx.GetAccessLogPath()
  83. if defaultLogPath != "" {
  84. logPath = defaultLogPath
  85. logger.Debugf("Using default access log path for preflight: %s", logPath)
  86. }
  87. }
  88. // Use service method to get preflight status
  89. result, err := service.GetPreflightStatus(logPath)
  90. if err != nil {
  91. cosy.ErrHandler(c, err)
  92. return
  93. }
  94. // Convert internal result to API response
  95. response := PreflightResponse{
  96. StartTime: &result.StartTime,
  97. EndTime: &result.EndTime,
  98. Available: result.Available,
  99. IndexStatus: result.IndexStatus,
  100. }
  101. logger.Debugf("Preflight response: log_path=%s, available=%v, index_status=%s",
  102. logPath, result.Available, result.IndexStatus)
  103. c.JSON(http.StatusOK, response)
  104. }
  105. // Note: GetIndexStatus function removed - index status is now included in GetLogList response
  106. // AdvancedSearchLogs provides advanced search capabilities for logs
  107. func AdvancedSearchLogs(c *gin.Context) {
  108. var req AdvancedSearchRequest
  109. if !cosy.BindAndValid(c, &req) {
  110. return
  111. }
  112. service := nginx_log.GetAnalyticsService()
  113. if service == nil {
  114. cosy.ErrHandler(c, nginx_log.ErrAnalyticsServiceNotAvailable)
  115. return
  116. }
  117. // Use default access log path if LogPath is empty
  118. if req.LogPath == "" {
  119. defaultLogPath := nginx.GetAccessLogPath()
  120. if defaultLogPath != "" {
  121. req.LogPath = defaultLogPath
  122. logger.Debugf("Using default access log path for search: %s", req.LogPath)
  123. }
  124. }
  125. // Validate log path if provided
  126. if req.LogPath != "" {
  127. if err := service.ValidateLogPath(req.LogPath); err != nil {
  128. cosy.ErrHandler(c, err)
  129. return
  130. }
  131. }
  132. // Build query request
  133. queryReq := &nginx_log.QueryRequest{
  134. StartTime: req.StartTime,
  135. EndTime: req.EndTime,
  136. Query: req.Query,
  137. IP: req.IP,
  138. Method: req.Method,
  139. Path: req.Path,
  140. UserAgent: req.UserAgent,
  141. Referer: req.Referer,
  142. Browser: req.Browser,
  143. OS: req.OS,
  144. Device: req.Device,
  145. Limit: req.Limit,
  146. Offset: req.Offset,
  147. SortBy: req.SortBy,
  148. SortOrder: req.SortOrder,
  149. LogPath: req.LogPath,
  150. }
  151. // Add status filter if provided
  152. if len(req.Status) > 0 {
  153. queryReq.Status = req.Status
  154. }
  155. // Execute search with timeout
  156. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
  157. defer cancel()
  158. result, err := service.SearchLogs(ctx, queryReq)
  159. if err != nil {
  160. cosy.ErrHandler(c, err)
  161. return
  162. }
  163. c.JSON(http.StatusOK, result)
  164. }
  165. // GetLogEntries provides simple log entry retrieval
  166. func GetLogEntries(c *gin.Context) {
  167. var req struct {
  168. Path string `json:"path" form:"path"`
  169. Limit int `json:"limit" form:"limit"`
  170. Tail bool `json:"tail" form:"tail"` // Get latest entries
  171. }
  172. if !cosy.BindAndValid(c, &req) {
  173. return
  174. }
  175. service := nginx_log.GetAnalyticsService()
  176. if service == nil {
  177. cosy.ErrHandler(c, nginx_log.ErrAnalyticsServiceNotAvailable)
  178. return
  179. }
  180. // Validate log path
  181. if err := service.ValidateLogPath(req.Path); err != nil {
  182. cosy.ErrHandler(c, err)
  183. return
  184. }
  185. // Get log entries
  186. entries, err := service.GetLogEntries(req.Path, req.Limit, req.Tail)
  187. if err != nil {
  188. cosy.ErrHandler(c, err)
  189. return
  190. }
  191. c.JSON(http.StatusOK, gin.H{
  192. "entries": entries,
  193. "count": len(entries),
  194. })
  195. }
  196. // DashboardRequest represents the request for dashboard analytics
  197. type DashboardRequest struct {
  198. LogPath string `json:"log_path" form:"log_path"`
  199. StartDate string `json:"start_date" form:"start_date"` // Format: 2006-01-02
  200. EndDate string `json:"end_date" form:"end_date"` // Format: 2006-01-02
  201. }
  202. // HourlyStats represents hourly UV/PV statistics
  203. type HourlyStats struct {
  204. Hour int `json:"hour"` // 0-23
  205. UV int `json:"uv"` // Unique visitors (unique IPs)
  206. PV int `json:"pv"` // Page views (total requests)
  207. Timestamp int64 `json:"timestamp"` // Unix timestamp for the hour
  208. }
  209. // DailyStats represents daily access statistics
  210. type DailyStats struct {
  211. Date string `json:"date"` // YYYY-MM-DD format
  212. UV int `json:"uv"` // Unique visitors
  213. PV int `json:"pv"` // Page views
  214. Timestamp int64 `json:"timestamp"` // Unix timestamp for the day
  215. }
  216. // URLStats represents URL access statistics
  217. type URLStats struct {
  218. URL string `json:"url"`
  219. Visits int `json:"visits"`
  220. Percent float64 `json:"percent"`
  221. }
  222. // BrowserStats represents browser statistics
  223. type BrowserStats struct {
  224. Browser string `json:"browser"`
  225. Count int `json:"count"`
  226. Percent float64 `json:"percent"`
  227. }
  228. // OSStats represents operating system statistics
  229. type OSStats struct {
  230. OS string `json:"os"`
  231. Count int `json:"count"`
  232. Percent float64 `json:"percent"`
  233. }
  234. // DeviceStats represents device type statistics
  235. type DeviceStats struct {
  236. Device string `json:"device"`
  237. Count int `json:"count"`
  238. Percent float64 `json:"percent"`
  239. }
  240. // DashboardResponse represents the dashboard analytics response
  241. type DashboardResponse struct {
  242. HourlyStats []HourlyStats `json:"hourly_stats"` // 24-hour UV/PV data
  243. DailyStats []DailyStats `json:"daily_stats"` // Monthly trend data
  244. TopURLs []URLStats `json:"top_urls"` // TOP 10 URLs
  245. Browsers []BrowserStats `json:"browsers"` // Browser statistics
  246. OperatingSystems []OSStats `json:"operating_systems"` // OS statistics
  247. Devices []DeviceStats `json:"devices"` // Device statistics
  248. Summary struct {
  249. TotalUV int `json:"total_uv"` // Total unique visitors
  250. TotalPV int `json:"total_pv"` // Total page views
  251. AvgDailyUV float64 `json:"avg_daily_uv"` // Average daily UV
  252. AvgDailyPV float64 `json:"avg_daily_pv"` // Average daily PV
  253. PeakHour int `json:"peak_hour"` // Peak traffic hour (0-23)
  254. PeakHourTraffic int `json:"peak_hour_traffic"` // Peak hour PV count
  255. } `json:"summary"`
  256. }
  257. // GetDashboardAnalytics provides comprehensive dashboard analytics from Bleve aggregations
  258. func GetDashboardAnalytics(c *gin.Context) {
  259. var req DashboardRequest
  260. // Parse JSON body for POST request
  261. if err := c.ShouldBindJSON(&req); err != nil {
  262. c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON request body: " + err.Error()})
  263. return
  264. }
  265. logger.Debugf("Dashboard API received log_path: '%s', start_date: '%s', end_date: '%s'", req.LogPath, req.StartDate, req.EndDate)
  266. service := nginx_log.GetAnalyticsService()
  267. if service == nil {
  268. cosy.ErrHandler(c, nginx_log.ErrAnalyticsServiceNotAvailable)
  269. return
  270. }
  271. // Use default access log path if LogPath is empty
  272. if req.LogPath == "" {
  273. defaultLogPath := nginx.GetAccessLogPath()
  274. if defaultLogPath != "" {
  275. req.LogPath = defaultLogPath
  276. logger.Debugf("Using default access log path: %s", req.LogPath)
  277. }
  278. }
  279. // Validate log path if provided
  280. if req.LogPath != "" {
  281. if err := service.ValidateLogPath(req.LogPath); err != nil {
  282. cosy.ErrHandler(c, err)
  283. return
  284. }
  285. }
  286. // Parse and validate date strings
  287. var startTime, endTime time.Time
  288. var err error
  289. if req.StartDate != "" {
  290. startTime, err = time.Parse("2006-01-02", req.StartDate)
  291. if err != nil {
  292. c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid start_date format, expected YYYY-MM-DD: " + err.Error()})
  293. return
  294. }
  295. }
  296. if req.EndDate != "" {
  297. endTime, err = time.Parse("2006-01-02", req.EndDate)
  298. if err != nil {
  299. c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid end_date format, expected YYYY-MM-DD: " + err.Error()})
  300. return
  301. }
  302. // Set end time to end of day
  303. endTime = endTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
  304. }
  305. // Set default time range if not provided (last 30 days)
  306. if startTime.IsZero() || endTime.IsZero() {
  307. endTime = time.Now()
  308. startTime = endTime.AddDate(0, 0, -30) // 30 days ago
  309. }
  310. // Get dashboard analytics with timeout
  311. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
  312. defer cancel()
  313. logger.Debugf("Dashboard request for log_path: %s, parsed start_time: %v, end_time: %v", req.LogPath, startTime, endTime)
  314. // Debug: Check time range from Bleve for this file
  315. debugStart, debugEnd := service.GetTimeRangeFromSummaryStatsForPath(req.LogPath)
  316. logger.Debugf("Bleve time range for %s - start=%v, end=%v", req.LogPath, debugStart, debugEnd)
  317. // Debug: Log exact query parameters
  318. queryRequest := &nginx_log.DashboardQueryRequest{
  319. LogPath: req.LogPath,
  320. StartTime: startTime.Unix(),
  321. EndTime: endTime.Unix(),
  322. }
  323. logger.Debugf("Query parameters - LogPath='%s', StartTime=%v, EndTime=%v",
  324. queryRequest.LogPath, queryRequest.StartTime, queryRequest.EndTime)
  325. // Get analytics from Bleve aggregations
  326. analytics, err := service.GetDashboardAnalyticsFromStats(ctx, queryRequest)
  327. if err != nil {
  328. cosy.ErrHandler(c, err)
  329. return
  330. }
  331. logger.Debugf("Successfully retrieved dashboard analytics from Bleve aggregations")
  332. // Debug: Log summary of results
  333. if analytics != nil {
  334. logger.Debugf("Results summary - TotalUV=%d, TotalPV=%d, HourlyStats=%d, DailyStats=%d, TopURLs=%d",
  335. analytics.Summary.TotalUV, analytics.Summary.TotalPV,
  336. len(analytics.HourlyStats), len(analytics.DailyStats), len(analytics.TopURLs))
  337. } else {
  338. logger.Debugf("Analytics result is nil")
  339. }
  340. c.JSON(http.StatusOK, analytics)
  341. }
  342. // GetWorldMapData provides geographic data for world map visualization
  343. func GetWorldMapData(c *gin.Context) {
  344. var req AnalyticsRequest
  345. if !cosy.BindAndValid(c, &req) {
  346. return
  347. }
  348. service := nginx_log.GetAnalyticsService()
  349. if service == nil {
  350. cosy.ErrHandler(c, nginx_log.ErrAnalyticsServiceNotAvailable)
  351. return
  352. }
  353. // Use default access log path if Path is empty
  354. if req.Path == "" {
  355. defaultLogPath := nginx.GetAccessLogPath()
  356. if defaultLogPath != "" {
  357. req.Path = defaultLogPath
  358. logger.Debugf("Using default access log path for world map: %s", req.Path)
  359. }
  360. }
  361. // Validate log path if provided
  362. if req.Path != "" {
  363. if err := service.ValidateLogPath(req.Path); err != nil {
  364. cosy.ErrHandler(c, err)
  365. return
  366. }
  367. }
  368. // Get world map data with timeout
  369. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
  370. defer cancel()
  371. data, err := service.GetWorldMapData(ctx, req.Path, time.Unix(req.StartTime, 0), time.Unix(req.EndTime, 0))
  372. if err != nil {
  373. cosy.ErrHandler(c, err)
  374. return
  375. }
  376. c.JSON(http.StatusOK, gin.H{
  377. "data": data,
  378. })
  379. }
  380. // GetChinaMapData provides geographic data for China map visualization
  381. func GetChinaMapData(c *gin.Context) {
  382. var req AnalyticsRequest
  383. if !cosy.BindAndValid(c, &req) {
  384. return
  385. }
  386. service := nginx_log.GetAnalyticsService()
  387. if service == nil {
  388. cosy.ErrHandler(c, nginx_log.ErrAnalyticsServiceNotAvailable)
  389. return
  390. }
  391. // Use default access log path if Path is empty
  392. if req.Path == "" {
  393. defaultLogPath := nginx.GetAccessLogPath()
  394. if defaultLogPath != "" {
  395. req.Path = defaultLogPath
  396. logger.Debugf("Using default access log path for China map: %s", req.Path)
  397. }
  398. }
  399. // Validate log path if provided
  400. if req.Path != "" {
  401. if err := service.ValidateLogPath(req.Path); err != nil {
  402. cosy.ErrHandler(c, err)
  403. return
  404. }
  405. }
  406. // Get China map data with timeout
  407. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
  408. defer cancel()
  409. data, err := service.GetChinaMapData(ctx, req.Path, time.Unix(req.StartTime, 0), time.Unix(req.EndTime, 0))
  410. if err != nil {
  411. cosy.ErrHandler(c, err)
  412. return
  413. }
  414. c.JSON(http.StatusOK, gin.H{
  415. "data": data,
  416. })
  417. }
  418. // GetGeoStats provides geographic statistics
  419. func GetGeoStats(c *gin.Context) {
  420. var req AnalyticsRequest
  421. if !cosy.BindAndValid(c, &req) {
  422. return
  423. }
  424. service := nginx_log.GetAnalyticsService()
  425. if service == nil {
  426. cosy.ErrHandler(c, nginx_log.ErrAnalyticsServiceNotAvailable)
  427. return
  428. }
  429. // Use default access log path if Path is empty
  430. if req.Path == "" {
  431. defaultLogPath := nginx.GetAccessLogPath()
  432. if defaultLogPath != "" {
  433. req.Path = defaultLogPath
  434. logger.Debugf("Using default access log path for geo stats: %s", req.Path)
  435. }
  436. }
  437. // Validate log path if provided
  438. if req.Path != "" {
  439. if err := service.ValidateLogPath(req.Path); err != nil {
  440. cosy.ErrHandler(c, err)
  441. return
  442. }
  443. }
  444. // Set default limit if not provided
  445. if req.Limit == 0 {
  446. req.Limit = 20
  447. }
  448. // Get geographic statistics with timeout
  449. ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
  450. defer cancel()
  451. stats, err := service.GetGeoStats(ctx, req.Path, time.Unix(req.StartTime, 0), time.Unix(req.EndTime, 0), req.Limit)
  452. if err != nil {
  453. cosy.ErrHandler(c, err)
  454. return
  455. }
  456. c.JSON(http.StatusOK, gin.H{
  457. "stats": stats,
  458. })
  459. }