simple_production_test.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. package nginx_log
  2. import (
  3. "context"
  4. "fmt"
  5. "math/rand"
  6. "os"
  7. "path/filepath"
  8. "testing"
  9. "time"
  10. "github.com/0xJacky/Nginx-UI/internal/nginx_log/indexer"
  11. "github.com/0xJacky/Nginx-UI/internal/nginx_log/parser"
  12. )
  13. // TestSimpleProductionThroughput tests realistic production throughput
  14. func TestSimpleProductionThroughput(t *testing.T) {
  15. if testing.Short() {
  16. t.Skip("Skipping production test in short mode")
  17. }
  18. recordCounts := []int{10000, 20000, 30000}
  19. for _, records := range recordCounts {
  20. t.Run(fmt.Sprintf("Records_%d", records), func(t *testing.T) {
  21. runSimpleProductionTest(t, records)
  22. })
  23. }
  24. }
  25. func runSimpleProductionTest(t *testing.T, recordCount int) {
  26. t.Logf("🚀 Testing production throughput with %d records", recordCount)
  27. // Create temp directory
  28. tempDir, err := os.MkdirTemp("", "simple_production_test_")
  29. if err != nil {
  30. t.Fatalf("Failed to create temp dir: %v", err)
  31. }
  32. defer os.RemoveAll(tempDir)
  33. // Generate test data
  34. testLogFile := filepath.Join(tempDir, "access.log")
  35. dataStart := time.Now()
  36. if err := generateSimpleLogFile(testLogFile, recordCount); err != nil {
  37. t.Fatalf("Failed to generate test data: %v", err)
  38. }
  39. dataTime := time.Since(dataStart)
  40. t.Logf("📊 Generated %d records in %v", recordCount, dataTime)
  41. // Setup production-like environment
  42. setupStart := time.Now()
  43. indexDir := filepath.Join(tempDir, "index")
  44. if err := os.MkdirAll(indexDir, 0755); err != nil {
  45. t.Fatalf("Failed to create index dir: %v", err)
  46. }
  47. config := indexer.DefaultIndexerConfig()
  48. config.IndexPath = indexDir
  49. config.WorkerCount = 12 // Reasonable for testing
  50. config.BatchSize = 1000 // Reasonable batch size
  51. config.EnableMetrics = true
  52. setupTime := time.Since(setupStart)
  53. t.Logf("⚙️ Setup completed in %v", setupTime)
  54. // Run the actual production test
  55. productionStart := time.Now()
  56. result := runActualProductionWorkflow(t, config, testLogFile, recordCount)
  57. productionTime := time.Since(productionStart)
  58. // Calculate metrics
  59. throughput := float64(recordCount) / productionTime.Seconds()
  60. t.Logf("🏆 === PRODUCTION RESULTS ===")
  61. t.Logf("📈 Records: %d", recordCount)
  62. t.Logf("⏱️ Total Time: %v", productionTime)
  63. t.Logf("🚀 Throughput: %.0f records/second", throughput)
  64. t.Logf("📊 Data Generation: %v", dataTime)
  65. t.Logf("⚙️ Setup Time: %v", setupTime)
  66. t.Logf("🔧 Processing Time: %v", productionTime)
  67. if result != nil {
  68. t.Logf("✅ Success Rate: %.1f%%", result.SuccessRate*100)
  69. t.Logf("📋 Processed/Succeeded: %d/%d", result.Processed, result.Succeeded)
  70. }
  71. }
  72. type SimpleResult struct {
  73. Processed int
  74. Succeeded int
  75. SuccessRate float64
  76. }
  77. func runActualProductionWorkflow(t *testing.T, config *indexer.Config, logFile string, expectedRecords int) *SimpleResult {
  78. // Create services like production
  79. geoService := &SimpleGeoIPService{}
  80. userAgentParser := parser.NewCachedUserAgentParser(
  81. parser.NewSimpleUserAgentParser(),
  82. 1000,
  83. )
  84. optimizedParser := parser.NewOptimizedParser(
  85. &parser.Config{
  86. MaxLineLength: 8 * 1024,
  87. WorkerCount: 8,
  88. BatchSize: 500,
  89. },
  90. userAgentParser,
  91. geoService,
  92. )
  93. // Create indexer
  94. shardManager := indexer.NewGroupedShardManager(config)
  95. indexerInstance := indexer.NewParallelIndexer(config, shardManager)
  96. ctx := context.Background()
  97. if err := indexerInstance.Start(ctx); err != nil {
  98. t.Fatalf("Failed to start indexer: %v", err)
  99. }
  100. defer indexerInstance.Stop()
  101. // Parse the log file
  102. file, err := os.Open(logFile)
  103. if err != nil {
  104. t.Fatalf("Failed to open log file: %v", err)
  105. }
  106. defer file.Close()
  107. parseResult, err := optimizedParser.OptimizedParseStream(ctx, file)
  108. if err != nil {
  109. t.Fatalf("Parsing failed: %v", err)
  110. }
  111. t.Logf("📋 Parsed %d records successfully", len(parseResult.Entries))
  112. // Index a subset of documents (to avoid timeout while still being realistic)
  113. maxToIndex := min(len(parseResult.Entries), 5000) // Limit for testing
  114. indexed := 0
  115. for i, entry := range parseResult.Entries[:maxToIndex] {
  116. doc := &indexer.Document{
  117. ID: fmt.Sprintf("doc_%d", i),
  118. Fields: &indexer.LogDocument{
  119. Timestamp: entry.Timestamp,
  120. IP: entry.IP,
  121. Method: entry.Method,
  122. Path: entry.Path,
  123. PathExact: entry.Path,
  124. Status: entry.Status,
  125. BytesSent: entry.BytesSent,
  126. Referer: entry.Referer,
  127. UserAgent: entry.UserAgent,
  128. Browser: entry.Browser,
  129. BrowserVer: entry.BrowserVer,
  130. OS: entry.OS,
  131. OSVersion: entry.OSVersion,
  132. DeviceType: entry.DeviceType,
  133. RequestTime: entry.RequestTime,
  134. FilePath: logFile,
  135. MainLogPath: logFile,
  136. Raw: entry.Raw,
  137. },
  138. }
  139. if err := indexerInstance.IndexDocument(ctx, doc); err == nil {
  140. indexed++
  141. }
  142. // Progress feedback
  143. if i%1000 == 0 && i > 0 {
  144. t.Logf("📊 Indexed %d documents...", i)
  145. }
  146. }
  147. // Flush
  148. if err := indexerInstance.FlushAll(); err != nil {
  149. t.Logf("Warning: Flush failed: %v", err)
  150. }
  151. return &SimpleResult{
  152. Processed: maxToIndex,
  153. Succeeded: indexed,
  154. SuccessRate: float64(indexed) / float64(maxToIndex),
  155. }
  156. }
  157. func generateSimpleLogFile(filename string, recordCount int) error {
  158. file, err := os.Create(filename)
  159. if err != nil {
  160. return err
  161. }
  162. defer file.Close()
  163. // use global rng defaults; no explicit rand.Seed needed in Go 1.20+
  164. baseTime := time.Now().Unix() - 3600 // 1 hour ago
  165. for i := 0; i < recordCount; i++ {
  166. timestamp := baseTime + int64(i%3600)
  167. ip := fmt.Sprintf("192.168.1.%d", rand.Intn(254)+1)
  168. path := []string{"/", "/api/users", "/api/data", "/health"}[rand.Intn(4)]
  169. status := []int{200, 200, 200, 404, 500}[rand.Intn(5)]
  170. size := rand.Intn(5000) + 100
  171. logLine := fmt.Sprintf(
  172. `%s - - [%s] "GET %s HTTP/1.1" %d %d "-" "Mozilla/5.0 Test" 0.123`,
  173. ip,
  174. time.Unix(timestamp, 0).Format("02/Jan/2006:15:04:05 -0700"),
  175. path,
  176. status,
  177. size,
  178. )
  179. if _, err := fmt.Fprintln(file, logLine); err != nil {
  180. return err
  181. }
  182. }
  183. return nil
  184. }
  185. type SimpleGeoIPService struct{}
  186. func (s *SimpleGeoIPService) Search(ip string) (*parser.GeoLocation, error) {
  187. return &parser.GeoLocation{
  188. CountryCode: "US",
  189. RegionCode: "CA",
  190. Province: "California",
  191. City: "San Francisco",
  192. }, nil
  193. }
  194. func min(a, b int) int {
  195. if a < b {
  196. return a
  197. }
  198. return b
  199. }