#!/bin/bash # PHP Analytics Library # Analyzes real usage data to make intelligent optimization decisions # Parses logs, process memory, and builds accurate domain profiles # ============================================================================ # ERROR LOG ANALYSIS - Find memory-related issues # ============================================================================ # Parse PHP-FPM error logs for memory exhaustion errors analyze_memory_errors_from_logs() { local username="$1" local domain="$2" local days="${3:-7}" local log_files log_files=$(find_php_error_logs "$username" "$domain") local memory_exhausted_count=0 local memory_limit_errors=0 local peak_memory_seen=0 # Look for memory exhaustion patterns while IFS= read -r log_file; do [ -z "$log_file" ] && continue [ ! -f "$log_file" ] && continue # Count "Allowed memory size exhausted" errors local exhausted_in_file exhausted_in_file=$(\grep -c "Allowed memory size of" "$log_file" 2>/dev/null || echo 0) exhausted_in_file=${exhausted_in_file##[[:space:]]} exhausted_in_file=${exhausted_in_file%%[[:space:]]} memory_exhausted_count=$((memory_exhausted_count + exhausted_in_file)) # Count memory limit exceeded local limit_errors_in_file limit_errors_in_file=$(\grep -c "memory_limit" "$log_file" 2>/dev/null || echo 0) limit_errors_in_file=${limit_errors_in_file##[[:space:]]} limit_errors_in_file=${limit_errors_in_file%%[[:space:]]} memory_limit_errors=$((memory_limit_errors + limit_errors_in_file)) # Extract peak memory from logs (format: "Allowed memory size of 134217728 bytes exhausted") local mem_values mem_values=$(\grep -o "Allowed memory size of [0-9]* bytes" "$log_file" 2>/dev/null | \grep -o "[0-9]*" | sort -rn | head -1) if [ -n "$mem_values" ]; then # Convert bytes to MB local mem_mb=$((mem_values / 1048576)) if [ "$mem_mb" -gt "$peak_memory_seen" ]; then peak_memory_seen=$mem_mb fi fi done <<< "$log_files" # Return: exhausted_count|limit_errors|peak_memory_mb echo "$memory_exhausted_count|$memory_limit_errors|$peak_memory_seen" } # Find PHP error log files for a domain find_php_error_logs() { local username="$1" local domain="$2" # cPanel locations if [ -d "/home/$username" ]; then find "/home/$username" -name "error_log" 2>/dev/null | head -5 fi # PHP-FPM error logs if [ -d "/var/log/php-fpm" ]; then find "/var/log/php-fpm" -name "*error*" 2>/dev/null | head -5 fi # Common log locations [ -f "/var/log/php.log" ] && echo "/var/log/php.log" [ -f "/var/log/php-errors.log" ] && echo "/var/log/php-errors.log" } # ============================================================================ # PROCESS MEMORY ANALYSIS - Measure actual memory usage # ============================================================================ # Analyze PHP process memory for a domain analyze_process_memory_usage() { local username="$1" # Get current running PHP processes for this user local processes processes=$(ps aux | \grep -E "php-fpm.*$username|_www.*php" | \grep -v grep) if [ -z "$processes" ]; then echo "0|0|0|0" # min|max|avg|count return fi local mem_values=() local min_mem=999999 local max_mem=0 local total_mem=0 local count=0 # Extract memory (RSS) from ps output while IFS= read -r line; do local rss=$(echo "$line" | awk '{print $6}') if [ -n "$rss" ] && [[ "$rss" =~ ^[0-9]+$ ]]; then mem_values+=("$rss") total_mem=$((total_mem + rss)) count=$((count + 1)) if [ "$rss" -lt "$min_mem" ]; then min_mem=$rss fi if [ "$rss" -gt "$max_mem" ]; then max_mem=$rss fi fi done <<< "$processes" if [ "$count" -eq 0 ]; then echo "0|0|0|0" return fi local avg_mem=$((total_mem / count)) # Convert to MB min_mem=$((min_mem / 1024)) max_mem=$((max_mem / 1024)) avg_mem=$((avg_mem / 1024)) # Return: min_mb|max_mb|avg_mb|count echo "$min_mem|$max_mem|$avg_mem|$count" } # ============================================================================ # TRAFFIC PATTERN ANALYSIS - Understand domain load # ============================================================================ # Get peak concurrent requests from access logs get_peak_concurrent_detailed() { local username="$1" local domain="$2" local log_file log_file=$(find_domain_access_log "$domain" "$username") if [ -z "$log_file" ] || [ ! -f "$log_file" ]; then echo "0|0|0" # peak|avg|stddev return fi # Analyze timestamps to find peak concurrency local timestamps timestamps=$(awk '{print $4}' "$log_file" 2>/dev/null | sed 's/\[//;s/\/.*//' | sort | uniq -c | sort -rn | head -1) local peak_concurrent=$(echo "$timestamps" | awk '{print $1}') peak_concurrent=${peak_concurrent:-0} # Calculate average concurrent local total_hits=$(wc -l < "$log_file") local unique_seconds=$(awk '{print $4}' "$log_file" 2>/dev/null | sed 's/\[//;s/\/.*//' | sort -u | wc -l) local avg_concurrent=0 if [ "$unique_seconds" -gt 0 ]; then avg_concurrent=$((total_hits / unique_seconds)) fi # Return: peak|avg|total_hits echo "$peak_concurrent|$avg_concurrent|$total_hits" } # ============================================================================ # MEMORY GROWTH DETECTION - Find memory leaks # ============================================================================ # Detect if domain has memory leak pattern detect_memory_leak_pattern() { local username="$1" local domain="$2" # Check error logs for progressive memory growth local error_analysis error_analysis=$(analyze_memory_errors_from_logs "$username" "$domain") local memory_exhausted_count=$(echo "$error_analysis" | cut -d'|' -f1) local peak_memory=$(echo "$error_analysis" | cut -d'|' -f3) # If many memory exhausted errors with growing peak memory, likely a leak if [ "$memory_exhausted_count" -gt 5 ] && [ "$peak_memory" -gt 200 ]; then echo "LIKELY_LEAK|High memory exhaustion errors ($memory_exhausted_count) detected" return 0 fi # Check if max_requests is 0 (process never recycled) local pool_config pool_config=$(find_fpm_pool_config "$username") if [ -n "$pool_config" ] && [ -f "$pool_config" ]; then local max_requests max_requests=$(\grep "^pm.max_requests" "$pool_config" | awk -F'=' '{print $2}' | tr -d ' ') if [ "$max_requests" = "0" ]; then echo "NEEDS_RECYCLING|pm.max_requests is disabled (0) - processes never recycled" return 0 fi fi echo "NO_LEAK|Normal memory patterns" return 1 } # ============================================================================ # DOMAIN PROFILE BUILDER - Comprehensive analysis # ============================================================================ # Build complete profile for a domain build_domain_profile() { local username="$1" local domain="$2" # Get memory errors local memory_errors memory_errors=$(analyze_memory_errors_from_logs "$username" "$domain") local mem_exhausted=$(echo "$memory_errors" | cut -d'|' -f1) local mem_limit_errors=$(echo "$memory_errors" | cut -d'|' -f2) local peak_mem_seen=$(echo "$memory_errors" | cut -d'|' -f3) # Get current process memory local process_mem process_mem=$(analyze_process_memory_usage "$username") local min_mem=$(echo "$process_mem" | cut -d'|' -f1) local max_mem=$(echo "$process_mem" | cut -d'|' -f2) local avg_mem=$(echo "$process_mem" | cut -d'|' -f3) local proc_count=$(echo "$process_mem" | cut -d'|' -f4) # Get traffic patterns local traffic traffic=$(get_peak_concurrent_detailed "$username" "$domain") local peak_concurrent=$(echo "$traffic" | cut -d'|' -f1) local avg_concurrent=$(echo "$traffic" | cut -d'|' -f2) local total_hits=$(echo "$traffic" | cut -d'|' -f3) # Detect memory leaks local leak_status leak_status=$(detect_memory_leak_pattern "$username" "$domain") local leak_type=$(echo "$leak_status" | cut -d'|' -f1) local leak_note=$(echo "$leak_status" | cut -d'|' -f2) # Get current settings local current_memory_limit current_memory_limit=$(get_effective_php_setting "$username" "memory_limit") local pool_config pool_config=$(find_fpm_pool_config "$username") local current_max_children="?" if [ -n "$pool_config" ] && [ -f "$pool_config" ]; then current_max_children=$(\grep "^pm.max_children" "$pool_config" | awk -F'=' '{print $2}' | tr -d ' ') fi # Format: domain|username|peak_concurrent|avg_concurrent|total_hits|min_mem|max_mem|avg_mem|proc_count|mem_exhausted|peak_mem_seen|leak_type|current_memory_limit|current_max_children echo "$domain|$username|$peak_concurrent|$avg_concurrent|$total_hits|$min_mem|$max_mem|$avg_mem|$proc_count|$mem_exhausted|$peak_mem_seen|$leak_type|$current_memory_limit|$current_max_children" } # ============================================================================ # INTELLIGENT RECOMMENDATIONS - Based on real data # ============================================================================ # Calculate memory_limit based on ACTUAL usage, not thresholds calculate_memory_limit_from_actual_usage() { local username="$1" local domain="$2" # Get real data local memory_errors memory_errors=$(analyze_memory_errors_from_logs "$username" "$domain") local peak_mem_seen=$(echo "$memory_errors" | cut -d'|' -f3) local process_mem process_mem=$(analyze_process_memory_usage "$username") local max_mem=$(echo "$process_mem" | cut -d'|' -f2) # Determine optimal memory_limit local recommended_memory=128 # If we've seen memory exhaustion, use observed peak + 20% buffer if [ "$peak_mem_seen" -gt 0 ]; then recommended_memory=$((peak_mem_seen + (peak_mem_seen / 5))) elif [ "$max_mem" -gt 0 ]; then # Use max observed process memory + 30% buffer for growth recommended_memory=$((max_mem + (max_mem / 3))) fi # Ensure minimum of 64M and maximum of 1024M [ "$recommended_memory" -lt 64 ] && recommended_memory=64 [ "$recommended_memory" -gt 1024 ] && recommended_memory=1024 echo "${recommended_memory}M" } # Calculate max_children based on ACTUAL peak concurrent calculate_max_children_from_actual_usage() { local username="$1" local domain="$2" # Get real peak concurrent from logs local traffic traffic=$(get_peak_concurrent_detailed "$username" "$domain") local peak_concurrent=$(echo "$traffic" | cut -d'|' -f1) # Add 30% safety margin for traffic spikes local recommended_max_children=$((peak_concurrent + (peak_concurrent / 3))) # Minimum of 5, maximum of 100 [ "$recommended_max_children" -lt 5 ] && recommended_max_children=5 [ "$recommended_max_children" -gt 100 ] && recommended_max_children=100 echo "$recommended_max_children" } # Calculate max_requests based on memory leak patterns calculate_max_requests_from_actual_usage() { local username="$1" local domain="$2" # Default: recycle every 500 requests local recommended_requests=500 # Check if memory leak detected local leak_status leak_status=$(detect_memory_leak_pattern "$username" "$domain") local leak_type=$(echo "$leak_status" | cut -d'|' -f1) # If leak detected, recycle more frequently if [ "$leak_type" = "LIKELY_LEAK" ]; then recommended_requests=250 # Recycle more often fi echo "$recommended_requests" } # ============================================================================ # PROFILE STORAGE AND RETRIEVAL # ============================================================================ # Store domain profile to file store_domain_profile() { local profile="$1" local profile_dir="/tmp/php-domain-profiles" mkdir -p "$profile_dir" 2>/dev/null local domain=$(echo "$profile" | cut -d'|' -f1) echo "$profile" > "$profile_dir/$domain.profile" } # Retrieve stored profile get_stored_profile() { local domain="$1" local profile_dir="/tmp/php-domain-profiles" [ -f "$profile_dir/$domain.profile" ] && cat "$profile_dir/$domain.profile" } # Get all stored profiles get_all_stored_profiles() { local profile_dir="/tmp/php-domain-profiles" [ -d "$profile_dir" ] && cat "$profile_dir"/*.profile 2>/dev/null } # Clear old profiles (older than 24 hours) cleanup_old_profiles() { local profile_dir="/tmp/php-domain-profiles" [ ! -d "$profile_dir" ] && return find "$profile_dir" -name "*.profile" -mtime +0 -delete 2>/dev/null } export -f analyze_memory_errors_from_logs export -f analyze_process_memory_usage export -f get_peak_concurrent_detailed export -f detect_memory_leak_pattern export -f build_domain_profile export -f calculate_memory_limit_from_actual_usage export -f calculate_max_children_from_actual_usage export -f calculate_max_requests_from_actual_usage export -f store_domain_profile export -f get_stored_profile export -f get_all_stored_profiles export -f cleanup_old_profiles