File: //opt/load-monitor.sh
#!/bin/bash
# ==========================================================
# cPanel Server Load & Resource Usage Report
# READ-ONLY MONITORING SCRIPT
#
# Shows:
# - System load average
# - CPU/memory usage
# - Top CPU processes
# - Top memory processes
# - Top 5 cPanel users by CPU/memory
# - MariaDB/MySQL live processlist
# - MariaDB/MySQL slow query log analysis
# - Apache process usage
# - LiteSpeed process usage
# - LSPHP per-account PHP workers
# - Exim mail queue / spam indicators
# - Disk I/O
# - Disk space
# - Inode usage
#
# READ-ONLY:
# - Does NOT restart services
# - Does NOT stop services
# - Does NOT kill processes
# - Does NOT modify databases
# - Does NOT modify MySQL/MariaDB configuration
# - Does NOT enable/disable slow query logging
# - Does NOT delete/freeze/thaw Exim mail
# - Does NOT force Exim delivery
# - Does NOT install packages
#
# The script DOES create:
# /var/log/server_load_reports/
# A timestamped report inside that directory
#
# Temporary slow-query analysis files are created under /tmp
# and removed after analysis.
#
# Usage:
# chmod +x server_load_report.sh
# sudo ./server_load_report.sh
#
# Optional:
# SLOW_QUERY_COUNT=100 sudo ./server_load_report.sh
#
# ==========================================================
###############################################################################
# CONFIGURATION
###############################################################################
REPORT_DIR="/var/log/server_load_reports"
# Number of slow-query entries to analyze.
#
# Default = 200
#
# Override:
# SLOW_QUERY_COUNT=500 sudo ./server_load_report.sh
#
SLOW_QUERY_COUNT="${SLOW_QUERY_COUNT:-200}"
###############################################################################
# ROOT CHECK
###############################################################################
if [ "$(id -u)" -ne 0 ]; then
echo "ERROR: This script must be run as root."
echo
echo "Usage:"
echo " sudo $0"
exit 1
fi
###############################################################################
# REPORT DIRECTORY
###############################################################################
mkdir -p "$REPORT_DIR" 2>/dev/null
if [ ! -d "$REPORT_DIR" ]; then
echo "ERROR: Unable to create report directory:"
echo "$REPORT_DIR"
exit 1
fi
REPORT="$REPORT_DIR/load_report_$(date +%Y%m%d_%H%M%S).log"
: > "$REPORT"
if [ ! -f "$REPORT" ]; then
echo "ERROR: Unable to create report:"
echo "$REPORT"
exit 1
fi
###############################################################################
# HELPER FUNCTIONS
###############################################################################
log() {
echo "$1" | tee -a "$REPORT"
}
section() {
{
echo
echo "###############################################################################"
echo "# $1"
echo "###############################################################################"
} | tee -a "$REPORT"
}
run() {
local output
output=$(eval "$1" 2>&1)
if [ -z "$output" ]; then
echo "(no output)" | tee -a "$REPORT"
else
echo "$output" | tee -a "$REPORT"
fi
}
###############################################################################
# HEADER
###############################################################################
log "SERVER LOAD REPORT"
log "============================================================"
log "Generated : $(date)"
log "Hostname : $(hostname)"
log "Report : $REPORT"
log "============================================================"
###############################################################################
# SYSTEM LOAD
###############################################################################
section "SYSTEM LOAD"
run "uptime"
if command -v nproc &> /dev/null; then
log "CPU cores: $(nproc)"
else
log "CPU cores: unavailable"
fi
if [ -r /proc/loadavg ]; then
log "Load average: $(cat /proc/loadavg)"
fi
###############################################################################
# MEMORY
###############################################################################
section "MEMORY USAGE"
if command -v free &> /dev/null; then
run "free -h"
else
log "free command not available."
fi
###############################################################################
# TOP PROCESSES BY CPU
###############################################################################
section "TOP 15 PROCESSES BY CPU"
run "ps -eo pid,ppid,user,%cpu,%mem,etime,cmd --sort=-%cpu | head -n 16"
###############################################################################
# TOP PROCESSES BY MEMORY
###############################################################################
section "TOP 15 PROCESSES BY MEMORY"
run "ps -eo pid,ppid,user,%cpu,%mem,etime,cmd --sort=-%mem | head -n 16"
###############################################################################
# TOP 5 USERS BY RESOURCE USAGE
###############################################################################
section "TOP 5 USERS BY RESOURCE USAGE (cPanel accounts)"
run "ps -eo user,%cpu,%mem --no-headers | awk '{
cpu[\$1] += \$2
mem[\$1] += \$3
count[\$1]++
}
END {
printf \"%-20s %-10s %-10s %-10s\n\", \"USER\", \"CPU%\", \"MEM%\", \"PROC_COUNT\"
for (u in cpu)
printf \"%-20s %-10.1f %-10.1f %-10d\n\", u, cpu[u], mem[u], count[u]
}' | (read -r header; echo \"\$header\"; sort -k2 -nr | head -n 5)"
###############################################################################
# MYSQL / MARIADB LIVE PROCESSLIST
###############################################################################
section "TOP MYSQL/MARIADB QUERIES (LIVE)"
DB_ADMIN=""
if command -v mariadbadmin &> /dev/null; then
DB_ADMIN="mariadbadmin"
elif command -v mysqladmin &> /dev/null; then
DB_ADMIN="mysqladmin"
fi
if [ -n "$DB_ADMIN" ]; then
log "Database admin utility: $DB_ADMIN"
run "$DB_ADMIN processlist --verbose 2>/dev/null | head -n 20"
else
log "Neither mariadbadmin nor mysqladmin was found."
fi
###############################################################################
# MYSQL / MARIADB SLOW QUERY LOG ANALYSIS
###############################################################################
section "MYSQL/MARIADB SLOW QUERY ANALYSIS"
###############################################################################
# Detect database client
###############################################################################
DB_CLI=""
if command -v mariadb &> /dev/null; then
DB_CLI="mariadb"
elif command -v mysql &> /dev/null; then
DB_CLI="mysql"
fi
###############################################################################
# Detect dumpslow utility
#
# Prefer MariaDB's:
# mariadb-dumpslow
#
# Fallback to MySQL's:
# mysqldumpslow
###############################################################################
DUMPSLOW_CMD=""
if command -v mariadb-dumpslow &> /dev/null; then
DUMPSLOW_CMD="$(command -v mariadb-dumpslow)"
elif command -v mysqldumpslow &> /dev/null; then
DUMPSLOW_CMD="$(command -v mysqldumpslow)"
fi
###############################################################################
# Get configured slow query log
###############################################################################
SLOW_LOG=""
if [ -n "$DB_CLI" ]; then
SLOW_LOG=$(
"$DB_CLI" -Nse \
"SHOW VARIABLES LIKE 'slow_query_log_file';" \
2>/dev/null |
awk '{print $2}'
)
fi
###############################################################################
# Fallback search for slow query log
###############################################################################
if [ -z "$SLOW_LOG" ] || [ ! -f "$SLOW_LOG" ]; then
SLOW_LOG=$(
find /var/lib/mysql /var/log \
-maxdepth 2 \
-type f \
\( -iname "*slow*.log" -o -iname "*-slow.log" \) \
2>/dev/null |
head -n 1
)
fi
###############################################################################
# Validate slow-query log and utility
###############################################################################
if [ -z "$SLOW_LOG" ] || [ ! -f "$SLOW_LOG" ]; then
log "Slow query log not found or not enabled on this server."
elif [ -z "$DUMPSLOW_CMD" ]; then
log "Neither mariadb-dumpslow nor mysqldumpslow was found."
log "Install the appropriate MariaDB/MySQL client utilities package."
else
log "Slow query log : $SLOW_LOG"
log "Slow query tool : $DUMPSLOW_CMD"
log "Entries analyzed : $SLOW_QUERY_COUNT"
###########################################################################
# Temporary file
###########################################################################
TMP_SLOW_LOG=$(mktemp /tmp/mysql_slow_analysis.XXXXXX)
trap 'rm -f "$TMP_SLOW_LOG"' EXIT
###########################################################################
# Extract complete slow-query entries
###########################################################################
awk -v max_entries="$SLOW_QUERY_COUNT" '
/^# Time:/ {
if (entry != "") {
entries[++count] = entry
}
entry = $0 "\n"
next
}
{
if (entry != "") {
entry = entry $0 "\n"
}
}
END {
if (entry != "") {
entries[++count] = entry
}
start = count - max_entries + 1
if (start < 1) {
start = 1
}
for (i = start; i <= count; i++) {
printf "%s", entries[i]
}
}
' "$SLOW_LOG" > "$TMP_SLOW_LOG"
###########################################################################
# Analyze
###########################################################################
if [ ! -s "$TMP_SLOW_LOG" ]; then
log "No complete slow-query entries were found."
else
log "Temporary analysis file created: $TMP_SLOW_LOG"
#######################################################################
# TOP BY QUERY TIME
#######################################################################
section "TOP 10 SLOW QUERY PATTERNS BY QUERY TIME"
run "\"$DUMPSLOW_CMD\" -s t -t 10 '$TMP_SLOW_LOG'"
#######################################################################
# TOP BY LOCK TIME
#######################################################################
section "TOP 10 SLOW QUERY PATTERNS BY LOCK TIME"
run "\"$DUMPSLOW_CMD\" -s l -t 10 '$TMP_SLOW_LOG'"
#######################################################################
# TOP BY ROWS SENT
#######################################################################
section "TOP 10 SLOW QUERY PATTERNS BY ROWS SENT"
run "\"$DUMPSLOW_CMD\" -s r -t 10 '$TMP_SLOW_LOG'"
#######################################################################
# TOP BY QUERY COUNT
#######################################################################
section "TOP 10 SLOW QUERY PATTERNS BY QUERY COUNT"
run "\"$DUMPSLOW_CMD\" -s c -t 10 '$TMP_SLOW_LOG'"
#######################################################################
# RECENT SLOW QUERIES
#######################################################################
section "RECENT SLOW QUERY ENTRIES (LATEST 10)"
run "awk '
/^# Time:/ {
if (entry != \"\") {
entries[++count] = entry
}
entry = \$0 \"\\n\"
next
}
{
if (entry != \"\") {
entry = entry \$0 \"\\n\"
}
}
END {
if (entry != \"\") {
entries[++count] = entry
}
start = count - 9
if (start < 1) {
start = 1
}
for (i = start; i <= count; i++) {
printf \"%s\", entries[i]
}
}
' '$TMP_SLOW_LOG'"
#######################################################################
# Cleanup
#######################################################################
rm -f "$TMP_SLOW_LOG"
trap - EXIT
log "Temporary analysis file removed."
fi
fi
###############################################################################
# APACHE
###############################################################################
section "APACHE STATUS"
if systemctl is-active --quiet httpd 2>/dev/null; then
log "Apache (httpd): ACTIVE"
run "ps -C httpd -o pid,user,%cpu,%mem,etime,cmd --sort=-%cpu | head -n 25"
elif systemctl is-active --quiet apache2 2>/dev/null; then
log "Apache (apache2): ACTIVE"
run "ps -C apache2 -o pid,user,%cpu,%mem,etime,cmd --sort=-%cpu | head -n 25"
else
log "Apache: NOT ACTIVE"
fi
###############################################################################
# LITESPEED
###############################################################################
section "LITESPEED STATUS"
if systemctl is-active --quiet lsws 2>/dev/null; then
log "LiteSpeed (lsws): ACTIVE"
run "ps -eo pid,user,%cpu,%mem,etime,cmd --sort=-%cpu | grep -iE 'litespeed|lshttpd' | grep -v grep | head -n 25"
else
log "LiteSpeed (lsws): NOT ACTIVE"
fi
###############################################################################
# LSPHP
###############################################################################
section "TOP 15 LSPHP PROCESSES (PER-ACCOUNT PHP WORKERS)"
run "ps -eo pid,user,%cpu,%mem,etime,cmd --sort=-%cpu | grep -i 'lsphp' | grep -v grep | head -n 15"
###############################################################################
# EXIM MAIL QUEUE / SPAM CHECK
#
# READ-ONLY:
# - Does NOT delete mail
# - Does NOT freeze/thaw mail
# - Does NOT force delivery
# - Does NOT retry delivery
# - Does NOT modify Exim configuration
# - Does NOT restart Exim
###############################################################################
section "EXIM MAIL QUEUE / SPAM CHECK"
if command -v exim &> /dev/null; then
log "Exim binary: $(command -v exim)"
###########################################################################
# TOTAL QUEUED MESSAGES
#
# exim -bpc reports the number of messages currently in the Exim queue.
###########################################################################
log ""
log "--- TOTAL QUEUED MESSAGES ---"
QUEUE_COUNT=$(exim -bpc 2>/dev/null)
if [[ "$QUEUE_COUNT" =~ ^[0-9]+$ ]]; then
log "Total queued messages: $QUEUE_COUNT"
if [ "$QUEUE_COUNT" -ge 1000 ]; then
log "WARNING: Exim queue is HIGH (1000+ messages)."
elif [ "$QUEUE_COUNT" -ge 500 ]; then
log "WARNING: Exim queue is elevated (500+ messages)."
elif [ "$QUEUE_COUNT" -ge 100 ]; then
log "NOTICE: Exim queue contains 100+ messages."
else
log "Exim queue size appears normal."
fi
else
log "Unable to determine Exim queue count."
fi
###########################################################################
# TOP SENDERS
#
# Uses the Exim main log.
#
# Example:
#
# grep "<=" /var/log/exim_mainlog |
# awk -F' <= ' '{print $2}' |
# awk '{print $1}' |
# sort |
# uniq -c |
# sort -nr |
# head -10
#
# This identifies the most frequently seen sender addresses in the
# Exim incoming/sending log.
###########################################################################
section "TOP 10 EXIM SENDERS"
EXIM_LOG="/var/log/exim_mainlog"
if [ -f "$EXIM_LOG" ]; then
log "Exim log: $EXIM_LOG"
log "Top senders based on '<=' entries:"
log ""
run "grep '<=' '$EXIM_LOG' 2>/dev/null | awk -F' <= ' '{print \$2}' | awk '{print \$1}' | sort | uniq -c | sort -nr | head -10"
else
log "Exim main log not found:"
log "$EXIM_LOG"
fi
else
log "Exim binary not found."
log "Exim mail queue check skipped."
fi
###############################################################################
# DISK I/O
###############################################################################
section "DISK I/O (5 SECOND SAMPLE)"
if command -v iostat &> /dev/null; then
run "iostat -x 1 2"
else
log "sysstat not installed - iostat unavailable."
log "This is informational only."
fi
###############################################################################
# DISK SPACE
###############################################################################
section "DISK SPACE USAGE"
run "df -hT"
###############################################################################
# INODE USAGE
###############################################################################
section "INODE USAGE"
run "df -ih"
###############################################################################
# END OF REPORT
###############################################################################
section "END OF REPORT"
log "Report completed: $(date)"
log "Full report saved to: $REPORT"
log "============================================================"