mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2026-05-31 00:19:44 +08:00
449 lines
14 KiB
Go
449 lines
14 KiB
Go
package nginx_log
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/0xJacky/Nginx-UI/internal/nginx_log/analytics"
|
|
"github.com/0xJacky/Nginx-UI/internal/nginx_log/parser"
|
|
"github.com/0xJacky/Nginx-UI/internal/nginx_log/utils"
|
|
)
|
|
|
|
// TestOptimizationSystemIntegration tests all optimization components working together
|
|
func TestOptimizationSystemIntegration(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping optimization integration test in short mode")
|
|
}
|
|
|
|
// Test data - realistic nginx log entries
|
|
testLogData := generateIntegrationTestData(1000)
|
|
ctx := context.Background()
|
|
|
|
t.Run("CompleteOptimizationPipeline", func(t *testing.T) {
|
|
// Test 1: Optimized Parser with all enhancements
|
|
config := parser.DefaultParserConfig()
|
|
config.MaxLineLength = 16 * 1024
|
|
config.BatchSize = 500
|
|
|
|
optimizedParser := parser.NewParser(
|
|
config,
|
|
parser.NewCachedUserAgentParser(parser.NewSimpleUserAgentParser(), 1000),
|
|
&mockGeoIPService{},
|
|
)
|
|
|
|
// Performance measurement
|
|
start := time.Now()
|
|
|
|
// Test ParseStream
|
|
parseResult, err := optimizedParser.ParseStream(ctx, strings.NewReader(testLogData))
|
|
if err != nil {
|
|
t.Fatalf("ParseStream failed: %v", err)
|
|
}
|
|
|
|
optimizedParseTime := time.Since(start)
|
|
optimizedRate := float64(parseResult.Processed) / optimizedParseTime.Seconds()
|
|
|
|
// Test 2: SIMD Parser performance
|
|
start = time.Now()
|
|
|
|
simdParser := parser.NewLogLineParser()
|
|
lines := strings.Split(testLogData, "\n")
|
|
logBytes := make([][]byte, 0, len(lines))
|
|
|
|
for _, line := range lines {
|
|
if strings.TrimSpace(line) != "" {
|
|
logBytes = append(logBytes, []byte(line))
|
|
}
|
|
}
|
|
|
|
simdEntries := simdParser.ParseLines(logBytes)
|
|
simdParseTime := time.Since(start)
|
|
simdRate := float64(len(simdEntries)) / simdParseTime.Seconds()
|
|
|
|
// Test 3: Enhanced Memory Pools under load
|
|
start = time.Now()
|
|
|
|
poolOperations := 10000
|
|
for i := 0; i < poolOperations; i++ {
|
|
// String builder pool
|
|
sb := utils.LogStringBuilderPool.Get()
|
|
sb.WriteString("integration test data")
|
|
utils.LogStringBuilderPool.Put(sb)
|
|
|
|
// Byte slice pool
|
|
slice := utils.GlobalByteSlicePool.Get(1024)
|
|
utils.GlobalByteSlicePool.Put(slice)
|
|
}
|
|
|
|
poolTime := time.Since(start)
|
|
poolRate := float64(poolOperations*2) / poolTime.Seconds() // 2 operations per iteration
|
|
|
|
// Test 4: Time-series analytics with optimizations
|
|
start = time.Now()
|
|
|
|
// Create realistic time-series data
|
|
timeSeriesData := make([]analytics.TimeValue, len(simdEntries))
|
|
baseTime := time.Now().Unix()
|
|
for i := range simdEntries {
|
|
timeSeriesData[i] = analytics.TimeValue{
|
|
Timestamp: baseTime + int64(i*60), // 1 minute intervals
|
|
Value: 1 + (i % 10), // Varied values
|
|
}
|
|
}
|
|
|
|
// Advanced analytics
|
|
advancedProcessor := analytics.NewAnomalyDetector()
|
|
anomalies := advancedProcessor.DetectAnomalies(timeSeriesData)
|
|
trend := advancedProcessor.CalculateTrend(timeSeriesData)
|
|
|
|
analyticsTime := time.Since(start)
|
|
|
|
// Test 5: Regex caching performance
|
|
start = time.Now()
|
|
|
|
cache := parser.GetGlobalRegexCache()
|
|
regexOperations := 10000
|
|
|
|
for i := 0; i < regexOperations; i++ {
|
|
// Should hit cache frequently
|
|
_, _ = cache.GetCommonRegex("ipv4")
|
|
_, _ = cache.GetCommonRegex("timestamp")
|
|
_, _ = cache.GetCommonRegex("status")
|
|
}
|
|
|
|
regexTime := time.Since(start)
|
|
regexRate := float64(regexOperations*3) / regexTime.Seconds()
|
|
|
|
// Performance assertions and reporting
|
|
t.Logf("=== OPTIMIZATION INTEGRATION RESULTS ===")
|
|
t.Logf("Optimized Parser: %d lines in %v (%.2f lines/sec)",
|
|
parseResult.Processed, optimizedParseTime, optimizedRate)
|
|
t.Logf("SIMD Parser: %d lines in %v (%.2f lines/sec)",
|
|
len(simdEntries), simdParseTime, simdRate)
|
|
t.Logf("Memory Pools: %d ops in %v (%.2f ops/sec)",
|
|
poolOperations*2, poolTime, poolRate)
|
|
t.Logf("Analytics: Processed in %v, %d anomalies, trend: %s",
|
|
analyticsTime, len(anomalies), trend.Direction)
|
|
t.Logf("Regex Cache: %d ops in %v (%.2f ops/sec)",
|
|
regexOperations*3, regexTime, regexRate)
|
|
|
|
// Performance requirements validation
|
|
minOptimizedRate := 500.0 // lines/sec
|
|
minSIMDRate := 10000.0 // lines/sec
|
|
minPoolRate := 100000.0 // ops/sec
|
|
minRegexRate := 1000000.0 // ops/sec
|
|
|
|
if optimizedRate < minOptimizedRate {
|
|
t.Errorf("Optimized parser rate %.2f < expected %.2f lines/sec",
|
|
optimizedRate, minOptimizedRate)
|
|
}
|
|
|
|
if simdRate < minSIMDRate {
|
|
t.Errorf("SIMD parser rate %.2f < expected %.2f lines/sec",
|
|
simdRate, minSIMDRate)
|
|
}
|
|
|
|
if poolRate < minPoolRate {
|
|
t.Errorf("Memory pool rate %.2f < expected %.2f ops/sec",
|
|
poolRate, minPoolRate)
|
|
}
|
|
|
|
if regexRate < minRegexRate {
|
|
t.Errorf("Regex cache rate %.2f < expected %.2f ops/sec",
|
|
regexRate, minRegexRate)
|
|
}
|
|
|
|
// Data integrity validation - both parsers should process same number of lines
|
|
expectedLines := len(strings.Split(strings.TrimSpace(testLogData), "\n"))
|
|
if parseResult.Processed != expectedLines {
|
|
t.Errorf("Optimized parser processed %d lines, expected %d",
|
|
parseResult.Processed, expectedLines)
|
|
}
|
|
|
|
if len(simdEntries) != expectedLines {
|
|
t.Errorf("SIMD parser processed %d lines, expected %d",
|
|
len(simdEntries), expectedLines)
|
|
}
|
|
|
|
// Test parsing consistency with a known log line
|
|
testLine := `192.168.1.100 - - [06/Sep/2025:10:00:00 +0000] "GET /test HTTP/1.1" 200 1024 "https://example.com" "Mozilla/5.0"`
|
|
|
|
// Parse with optimized parser
|
|
optimizedTest, err := optimizedParser.ParseStream(ctx, strings.NewReader(testLine))
|
|
if err != nil || len(optimizedTest.Entries) == 0 {
|
|
t.Errorf("Optimized parser failed on test line: %v", err)
|
|
} else {
|
|
// Parse with SIMD parser
|
|
simdTestEntry := simdParser.ParseLine([]byte(testLine))
|
|
if simdTestEntry == nil {
|
|
t.Error("SIMD parser failed on test line")
|
|
} else {
|
|
// Compare the results
|
|
optimizedTestEntry := optimizedTest.Entries[0]
|
|
if optimizedTestEntry.IP != simdTestEntry.IP {
|
|
t.Errorf("Test line IP mismatch: optimized=%s, simd=%s",
|
|
optimizedTestEntry.IP, simdTestEntry.IP)
|
|
}
|
|
if optimizedTestEntry.Status != simdTestEntry.Status {
|
|
t.Errorf("Test line status mismatch: optimized=%d, simd=%d",
|
|
optimizedTestEntry.Status, simdTestEntry.Status)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("ResourceEfficiencyValidation", func(t *testing.T) {
|
|
// Test memory pool efficiency
|
|
poolManager := utils.GetGlobalPoolManager()
|
|
_ = poolManager.GetAllStats() // Get initial stats for reference
|
|
|
|
// Perform intensive operations
|
|
iterations := 5000
|
|
for i := 0; i < iterations; i++ {
|
|
// String builder operations
|
|
sb := utils.LogStringBuilderPool.Get()
|
|
sb.WriteString("efficiency test data for memory pools")
|
|
utils.LogStringBuilderPool.Put(sb)
|
|
|
|
// Byte slice operations
|
|
slice := utils.GlobalByteSlicePool.Get(2048)
|
|
copy(slice[:10], []byte("test data"))
|
|
utils.GlobalByteSlicePool.Put(slice)
|
|
|
|
// Worker operations
|
|
worker := utils.NewPooledWorker()
|
|
testData := []byte("worker efficiency test")
|
|
worker.ProcessWithPools(testData, func(data []byte, sb *strings.Builder) error {
|
|
sb.WriteString(string(data))
|
|
return nil
|
|
})
|
|
worker.Cleanup()
|
|
}
|
|
|
|
finalStats := poolManager.GetAllStats()
|
|
|
|
// Verify pool efficiency
|
|
t.Logf("=== POOL EFFICIENCY RESULTS ===")
|
|
for name, stats := range finalStats {
|
|
if statsObj, ok := stats.(map[string]interface{}); ok {
|
|
t.Logf("Pool %s: %+v", name, statsObj)
|
|
} else {
|
|
t.Logf("Pool %s: %+v", name, stats)
|
|
}
|
|
}
|
|
|
|
// Test regex cache efficiency
|
|
cache := parser.GetGlobalRegexCache()
|
|
initialCacheStats := cache.GetStats()
|
|
|
|
// Perform regex operations
|
|
for i := 0; i < 1000; i++ {
|
|
_, _ = cache.GetCommonRegex("ipv4")
|
|
_, _ = cache.GetCommonRegex("timestamp")
|
|
_, _ = cache.GetCommonRegex("combined_format")
|
|
}
|
|
|
|
finalCacheStats := cache.GetStats()
|
|
|
|
hitRateImprovement := finalCacheStats.HitRate - initialCacheStats.HitRate
|
|
|
|
t.Logf("=== CACHE EFFICIENCY RESULTS ===")
|
|
t.Logf("Initial: Hits=%d, Misses=%d, HitRate=%.2f%%",
|
|
initialCacheStats.Hits, initialCacheStats.Misses, initialCacheStats.HitRate*100)
|
|
t.Logf("Final: Hits=%d, Misses=%d, HitRate=%.2f%%",
|
|
finalCacheStats.Hits, finalCacheStats.Misses, finalCacheStats.HitRate*100)
|
|
t.Logf("Hit rate improvement: %.2f%%", hitRateImprovement*100)
|
|
|
|
// Cache hit rate should be very high
|
|
if finalCacheStats.HitRate < 0.9 {
|
|
t.Errorf("Cache hit rate %.2f%% is too low, expected > 90%%",
|
|
finalCacheStats.HitRate*100)
|
|
}
|
|
})
|
|
|
|
t.Run("StressTestOptimizations", func(t *testing.T) {
|
|
// Stress test with concurrent operations
|
|
concurrency := 10
|
|
operationsPerGoroutine := 1000
|
|
|
|
// Channel to collect results
|
|
results := make(chan time.Duration, concurrency*3)
|
|
|
|
// Start concurrent goroutines
|
|
for i := 0; i < concurrency; i++ {
|
|
go func(id int) {
|
|
// SIMD parsing stress test
|
|
start := time.Now()
|
|
simdParser := parser.NewLogLineParser()
|
|
|
|
testLine := `192.168.1.100 - - [06/Sep/2025:10:00:00 +0000] "GET /stress HTTP/1.1" 200 1024 "https://test.com" "StressTest/1.0"`
|
|
|
|
for j := 0; j < operationsPerGoroutine; j++ {
|
|
_ = simdParser.ParseLine([]byte(testLine))
|
|
}
|
|
results <- time.Since(start)
|
|
|
|
// Memory pool stress test
|
|
start = time.Now()
|
|
for j := 0; j < operationsPerGoroutine; j++ {
|
|
sb := utils.LogStringBuilderPool.Get()
|
|
sb.WriteString("concurrent stress test")
|
|
utils.LogStringBuilderPool.Put(sb)
|
|
}
|
|
results <- time.Since(start)
|
|
|
|
// Regex cache stress test
|
|
start = time.Now()
|
|
cache := parser.GetGlobalRegexCache()
|
|
for j := 0; j < operationsPerGoroutine; j++ {
|
|
_, _ = cache.GetCommonRegex("ipv4")
|
|
}
|
|
results <- time.Since(start)
|
|
}(i)
|
|
}
|
|
|
|
// Collect results
|
|
totalDuration := time.Duration(0)
|
|
resultCount := 0
|
|
timeout := time.After(30 * time.Second)
|
|
|
|
for resultCount < concurrency*3 {
|
|
select {
|
|
case duration := <-results:
|
|
totalDuration += duration
|
|
resultCount++
|
|
case <-timeout:
|
|
t.Fatal("Stress test timed out")
|
|
}
|
|
}
|
|
|
|
averageDuration := totalDuration / time.Duration(resultCount)
|
|
totalOperations := concurrency * operationsPerGoroutine * 3
|
|
overallRate := float64(totalOperations) / totalDuration.Seconds()
|
|
|
|
t.Logf("=== STRESS TEST RESULTS ===")
|
|
t.Logf("Total operations: %d", totalOperations)
|
|
t.Logf("Total time: %v", totalDuration)
|
|
t.Logf("Average time per goroutine: %v", averageDuration)
|
|
t.Logf("Overall rate: %.2f ops/sec", overallRate)
|
|
|
|
// Performance assertion
|
|
minStressRate := 10000.0 // ops/sec under stress
|
|
if overallRate < minStressRate {
|
|
t.Errorf("Stress test rate %.2f < expected %.2f ops/sec",
|
|
overallRate, minStressRate)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestOptimizationCorrectness validates that all optimizations produce correct results
|
|
func TestOptimizationCorrectness(t *testing.T) {
|
|
testLogLine := `127.0.0.1 - - [06/Sep/2025:10:00:00 +0000] "GET /test.html HTTP/1.1" 200 2048 "https://example.com" "Mozilla/5.0"`
|
|
ctx := context.Background()
|
|
|
|
// Test all parsing methods produce identical results
|
|
t.Run("ParsingMethodConsistency", func(t *testing.T) {
|
|
// Standard parser
|
|
config := parser.DefaultParserConfig()
|
|
config.MaxLineLength = 16 * 1024
|
|
standardParser := parser.NewParser(
|
|
config,
|
|
parser.NewSimpleUserAgentParser(),
|
|
&mockGeoIPService{},
|
|
)
|
|
|
|
standardResult, err := standardParser.ParseStream(ctx, strings.NewReader(testLogLine))
|
|
if err != nil || len(standardResult.Entries) == 0 {
|
|
t.Fatalf("Standard parser failed: %v", err)
|
|
}
|
|
|
|
// Optimized parser
|
|
optimizedResult, err := standardParser.ParseStream(ctx, strings.NewReader(testLogLine))
|
|
if err != nil || len(optimizedResult.Entries) == 0 {
|
|
t.Fatalf("Optimized parser failed: %v", err)
|
|
}
|
|
|
|
// SIMD parser
|
|
simdParser := parser.NewLogLineParser()
|
|
simdEntry := simdParser.ParseLine([]byte(testLogLine))
|
|
if simdEntry == nil {
|
|
t.Fatal("SIMD parser returned nil")
|
|
}
|
|
|
|
// Compare results
|
|
standardEntry := standardResult.Entries[0]
|
|
optimizedEntry := optimizedResult.Entries[0]
|
|
|
|
// Verify consistency across all methods
|
|
if standardEntry.IP != optimizedEntry.IP || standardEntry.IP != simdEntry.IP {
|
|
t.Errorf("IP inconsistency: standard=%s, optimized=%s, simd=%s",
|
|
standardEntry.IP, optimizedEntry.IP, simdEntry.IP)
|
|
}
|
|
|
|
if standardEntry.Status != optimizedEntry.Status || standardEntry.Status != simdEntry.Status {
|
|
t.Errorf("Status inconsistency: standard=%d, optimized=%d, simd=%d",
|
|
standardEntry.Status, optimizedEntry.Status, simdEntry.Status)
|
|
}
|
|
|
|
t.Logf("All parsing methods produce consistent results: IP=%s, Status=%d",
|
|
standardEntry.IP, standardEntry.Status)
|
|
})
|
|
}
|
|
|
|
// generateIntegrationTestData creates realistic test data for integration testing
|
|
func generateIntegrationTestData(lines int) string {
|
|
var builder strings.Builder
|
|
builder.Grow(lines * 200) // Pre-allocate space
|
|
|
|
ips := []string{"127.0.0.1", "192.168.1.100", "10.0.0.50", "203.0.113.195", "172.16.0.25"}
|
|
methods := []string{"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS"}
|
|
paths := []string{"/", "/index.html", "/api/data", "/styles/main.css", "/js/app.js", "/api/users", "/login", "/dashboard"}
|
|
statuses := []int{200, 201, 204, 301, 302, 400, 401, 403, 404, 429, 500, 502, 503}
|
|
userAgents := []string{
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
|
"curl/7.68.0",
|
|
"PostmanRuntime/7.29.0",
|
|
"Integration-Test/1.0",
|
|
}
|
|
|
|
baseTime := time.Date(2025, 9, 6, 10, 0, 0, 0, time.UTC).Unix()
|
|
|
|
for i := 0; i < lines; i++ {
|
|
ip := ips[i%len(ips)]
|
|
timestamp := time.Unix(baseTime+int64(i*60), 0).Format("02/Jan/2006:15:04:05 -0700")
|
|
method := methods[i%len(methods)]
|
|
path := paths[i%len(paths)]
|
|
status := statuses[i%len(statuses)]
|
|
size := 1000 + (i % 10000)
|
|
userAgent := userAgents[i%len(userAgents)]
|
|
|
|
line := ip + ` - - [` + timestamp + `] "` + method + ` ` + path + ` HTTP/1.1" ` +
|
|
string(rune('0'+status/100)) + string(rune('0'+(status/10)%10)) + string(rune('0'+status%10)) + ` ` +
|
|
string(rune('0'+size/10000)) + string(rune('0'+(size/1000)%10)) +
|
|
string(rune('0'+(size/100)%10)) + string(rune('0'+(size/10)%10)) + string(rune('0'+size%10)) +
|
|
` "https://example.com" "` + userAgent + `"`
|
|
|
|
builder.WriteString(line)
|
|
if i < lines-1 {
|
|
builder.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
return builder.String()
|
|
}
|
|
|
|
// mockGeoIPService provides mock geo IP functionality
|
|
type mockGeoIPService struct{}
|
|
|
|
func (m *mockGeoIPService) Search(ip string) (*parser.GeoLocation, error) {
|
|
return &parser.GeoLocation{
|
|
CountryCode: "US",
|
|
RegionCode: "CA",
|
|
Province: "California",
|
|
City: "San Francisco",
|
|
}, nil
|
|
} |