production_accurate_test.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. package nginx_log
  2. import (
  3. "context"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "testing"
  8. "time"
  9. "github.com/0xJacky/Nginx-UI/internal/nginx_log/indexer"
  10. )
  11. // TestAccurateProductionPerformance tests the exact same workflow as production rebuild
  12. func TestAccurateProductionPerformance(t *testing.T) {
  13. if testing.Short() {
  14. t.Skip("Skipping accurate production performance test in short mode")
  15. }
  16. // Test with realistic scales matching your production usage
  17. testSizes := []struct {
  18. name string
  19. records int
  20. }{
  21. {"Production_50K", 50000}, // Smaller scale for quick validation
  22. {"Production_100K", 100000}, // Medium scale
  23. }
  24. for _, testSize := range testSizes {
  25. t.Run(testSize.name, func(t *testing.T) {
  26. runAccurateProductionTest(t, testSize.records)
  27. })
  28. }
  29. }
  30. func runAccurateProductionTest(t *testing.T, recordCount int) {
  31. t.Logf("🚀 Starting ACCURATE production test with %d records (same as production rebuild)", recordCount)
  32. tempDir := t.TempDir()
  33. // Generate test data with production-like log entries
  34. testLogFile := filepath.Join(tempDir, "access.log")
  35. dataGenStart := time.Now()
  36. if err := generateProductionLikeLogFile(testLogFile, recordCount); err != nil {
  37. t.Fatalf("Failed to generate test data: %v", err)
  38. }
  39. dataGenTime := time.Since(dataGenStart)
  40. t.Logf("📊 Generated %d records in %v", recordCount, dataGenTime)
  41. // Create indexer with PRODUCTION configuration (not test configuration)
  42. indexDir := filepath.Join(tempDir, "index")
  43. if err := os.MkdirAll(indexDir, 0755); err != nil {
  44. t.Fatalf("Failed to create index dir: %v", err)
  45. }
  46. // Use production default configuration
  47. config := indexer.DefaultIndexerConfig()
  48. config.IndexPath = indexDir
  49. // Don't override the optimized defaults - use them as-is
  50. shardManager := indexer.NewGroupedShardManager(config)
  51. parallelIndexer := indexer.NewParallelIndexer(config, shardManager)
  52. ctx := context.Background()
  53. if err := parallelIndexer.Start(ctx); err != nil {
  54. t.Fatalf("Failed to start indexer: %v", err)
  55. }
  56. defer parallelIndexer.Stop()
  57. // Now use EXACT same method as production: IndexLogGroupWithProgress
  58. t.Logf("🔄 Starting production rebuild using IndexLogGroupWithProgress")
  59. productionStart := time.Now()
  60. // Create progress config (similar to production but with test logging)
  61. progressConfig := &indexer.ProgressConfig{
  62. NotifyInterval: 1 * time.Second,
  63. OnProgress: func(progress indexer.ProgressNotification) {
  64. t.Logf("📈 Progress: %.1f%% - Files: %d/%d, Lines: %d/%d",
  65. progress.Percentage, progress.CompletedFiles, progress.TotalFiles,
  66. progress.ProcessedLines, progress.EstimatedLines)
  67. },
  68. OnCompletion: func(completion indexer.CompletionNotification) {
  69. t.Logf("✅ Completed: %s - Success: %t, Duration: %s, Lines: %d",
  70. completion.LogGroupPath, completion.Success, completion.Duration, completion.TotalLines)
  71. },
  72. }
  73. // Call the EXACT same method as production
  74. docsCountMap, minTime, maxTime, err := parallelIndexer.IndexLogGroupWithProgress(testLogFile, progressConfig)
  75. productionTime := time.Since(productionStart)
  76. if err != nil {
  77. t.Fatalf("IndexLogGroupWithProgress failed: %v", err)
  78. }
  79. // Calculate metrics (same as production rebuild reporting)
  80. var totalIndexedDocs uint64
  81. for _, count := range docsCountMap {
  82. totalIndexedDocs += count
  83. }
  84. throughput := float64(totalIndexedDocs) / productionTime.Seconds()
  85. t.Logf("🏆 === ACCURATE PRODUCTION RESULTS ===")
  86. t.Logf("📊 Input Records: %d", recordCount)
  87. t.Logf("📋 Documents Indexed: %d", totalIndexedDocs)
  88. t.Logf("⏱️ Total Time: %s", productionTime)
  89. t.Logf("🚀 Throughput: %.0f records/second", throughput)
  90. t.Logf("📈 Files Processed: %d", len(docsCountMap))
  91. if minTime != nil && maxTime != nil {
  92. t.Logf("📅 Time Range: %s to %s", minTime.Format(time.RFC3339), maxTime.Format(time.RFC3339))
  93. }
  94. // Flush all data (same as production)
  95. if err := parallelIndexer.FlushAll(); err != nil {
  96. t.Logf("Warning: Flush failed: %v", err)
  97. }
  98. // Performance validation
  99. if throughput < 1000 {
  100. t.Errorf("⚠️ Throughput too low: %.0f records/sec (expected >1000 for production)", throughput)
  101. }
  102. if totalIndexedDocs == 0 {
  103. t.Errorf("❌ No documents were indexed")
  104. }
  105. t.Logf("✨ Test completed successfully - Production performance validated")
  106. }
  107. func generateProductionLikeLogFile(filename string, recordCount int) error {
  108. file, err := os.Create(filename)
  109. if err != nil {
  110. return err
  111. }
  112. defer file.Close()
  113. baseTime := time.Now().Unix() - 86400 // 24 hours ago
  114. // Production-like variety in IPs, paths, user agents, etc.
  115. ips := []string{
  116. "192.168.1.100", "10.0.0.45", "172.16.0.25", "203.0.113.45",
  117. "198.51.100.67", "192.0.2.89", "203.0.113.234", "198.51.100.123",
  118. "10.0.1.45", "192.168.2.78", "172.16.1.99", "10.0.2.156",
  119. }
  120. methods := []string{"GET", "POST", "PUT", "DELETE", "HEAD"}
  121. paths := []string{
  122. "/", "/api/users", "/api/posts", "/api/auth/login", "/api/data",
  123. "/static/css/main.css", "/static/js/app.js", "/images/logo.png",
  124. "/admin/dashboard", "/user/profile", "/search", "/api/v1/metrics",
  125. "/health", "/favicon.ico", "/robots.txt", "/sitemap.xml",
  126. }
  127. userAgents := []string{
  128. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/91.0.4472.124",
  129. "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Safari/605.1.15",
  130. "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/91.0.4472.124",
  131. "Mozilla/5.0 (iPhone; CPU iPhone OS 14_6) AppleWebKit/605.1.15 Mobile Safari/604.1",
  132. "Mozilla/5.0 (Android 11; Mobile) AppleWebKit/537.36 Chrome/91.0.4472.124",
  133. }
  134. statuses := []int{200, 200, 200, 200, 200, 304, 301, 404, 500} // Weighted toward 200
  135. for i := 0; i < recordCount; i++ {
  136. // Distribute timestamps over 24 hours
  137. timestamp := baseTime + int64(i%86400)
  138. ip := ips[i%len(ips)]
  139. method := methods[i%len(methods)]
  140. path := paths[i%len(paths)]
  141. status := statuses[i%len(statuses)]
  142. size := 500 + (i % 5000) // Vary response sizes
  143. userAgent := userAgents[i%len(userAgents)]
  144. referer := "-"
  145. if i%10 == 0 { // 10% of requests have referrer
  146. referer = "https://example.com/page"
  147. }
  148. requestTime := float64(i%2000) / 1000.0 // 0-2 seconds
  149. // Standard nginx combined log format (same as production)
  150. logLine := fmt.Sprintf(
  151. `%s - - [%s] "%s %s HTTP/1.1" %d %d "%s" "%s" %.3f`,
  152. ip,
  153. time.Unix(timestamp, 0).Format("02/Jan/2006:15:04:05 -0700"),
  154. method,
  155. path,
  156. status,
  157. size,
  158. referer,
  159. userAgent,
  160. requestTime,
  161. )
  162. if _, err := fmt.Fprintln(file, logLine); err != nil {
  163. return err
  164. }
  165. }
  166. return nil
  167. }