package nginx_log import ( "path/filepath" "regexp" "strings" "time" "github.com/0xJacky/Nginx-UI/internal/cache" "github.com/0xJacky/Nginx-UI/internal/nginx" "github.com/0xJacky/Nginx-UI/internal/nginx_log/utils" "github.com/uozi-tech/cosy/logger" ) // Regular expression for log directives - matches access_log or error_log var ( logDirectiveRegex = regexp.MustCompile(`(?m)(access_log|error_log)\s+([^\s;]+)(?:\s+[^;]+)?;`) ) // Use init function to automatically register callback func init() { // Register the callback directly with the global registry cache.RegisterCallback("nginx_log.scanForLogDirectives", scanForLogDirectives) } // scanForLogDirectives scans and parses configuration files for log directives func scanForLogDirectives(configPath string, content []byte) error { // Step 1: Get nginx prefix prefix := nginx.GetPrefix() // Step 2: Remove existing log paths - with timeout protection removeSuccess := make(chan bool, 1) go func() { defer func() { if r := recover(); r != nil { logger.Warnf("[scanForLogDirectives] RemoveLogPathsFromConfig panicked for %s: %v", configPath, r) } }() RemoveLogPathsFromConfig(configPath) removeSuccess <- true }() select { case <-removeSuccess: // Success - no logging needed case <-time.After(2 * time.Second): logger.Warnf("[scanForLogDirectives] RemoveLogPathsFromConfig timed out after 2 seconds for config: %s", configPath) } // Step 3: Find log directives using regex matches := logDirectiveRegex.FindAllSubmatch(content, -1) // Step 4: Parse log paths for i, match := range matches { if len(match) >= 3 { // Check if this match is from a commented line if isCommentedMatch(content, match) { continue // Skip commented directives } directiveType := string(match[1]) // "access_log" or "error_log" logPath := string(match[2]) // Path to log file // Skip if log is disabled with "off" if logPath == "off" { continue } // Handle relative paths by joining with nginx prefix if !filepath.IsAbs(logPath) { logPath = filepath.Join(prefix, logPath) } // Validate log path if utils.IsValidLogPath(logPath) { logType := "access" if directiveType == "error_log" { logType = "error" } // Add to cache with config file path - with timeout protection addSuccess := make(chan bool, 1) go func() { defer func() { if r := recover(); r != nil { logger.Warnf("[scanForLogDirectives] AddLogPath panicked for match %d, path %s: %v", i, logPath, r) } }() AddLogPath(logPath, logType, filepath.Base(logPath), configPath) addSuccess <- true }() select { case <-addSuccess: // Success - no logging needed case <-time.After(1 * time.Second): logger.Warnf("[scanForLogDirectives] AddLogPath timed out after 1 second for match %d, path %s", i, logPath) } } } } return nil } // isCommentedMatch checks if a regex match is from a commented line func isCommentedMatch(content []byte, match [][]byte) bool { // Find the position of the match in the content matchStr := string(match[0]) matchIndex := strings.Index(string(content), matchStr) if matchIndex == -1 { return false } // Find the start of the line containing this match lineStart := matchIndex for lineStart > 0 && content[lineStart-1] != '\n' { lineStart-- } // Check if the line starts with # (possibly with leading whitespace) for i := lineStart; i < matchIndex; i++ { char := content[i] if char == '#' { return true // This is a commented line } if char != ' ' && char != '\t' { return false // Found non-whitespace before the directive, not a comment } } return false }