This capstone project synthesizes everything you have learned across all four modules into a single, production-quality tool: a System Health Monitor.
The script will:
- ✅ Check CPU, memory, and disk usage
- ✅ Monitor critical services
- ✅ Detect abnormal processes (high CPU/RAM)
- ✅ Generate a timestamped HTML report
- ✅ Alert when thresholds are exceeded
- ✅ Be schedulable via cron
- ✅ Be fully error-handled and logged
flowchart TD
A[▶️ health_monitor.sh] --> B[Load Config]
B --> C[Initialize Logging]
C --> D{Check CPU}
C --> E{Check Memory}
C --> F{Check Disk}
C --> G{Check Services}
D --> H[Generate Report Section]
E --> H
F --> H
G --> H
H --> I[Build HTML Report]
I --> J{Threshold Exceeded?}
J -- Yes --> K[📧 Send Alert]
J -- No --> L[💾 Save Report]
K --> L
L --> M[✅ Done]
~/scripts/health-monitor/
├── health_monitor.sh ← Main script
├── config.sh ← Thresholds and settings
├── lib/
│ └── report.sh ← HTML report functions
└── reports/ ← Generated HTML reports (auto-created)
#!/usr/bin/env bash
# config.sh — System Health Monitor Configuration
# Alert Thresholds
readonly CPU_THRESHOLD=80 # Alert if CPU > 80%
readonly MEM_THRESHOLD=85 # Alert if memory > 85%
readonly DISK_THRESHOLD=90 # Alert if any disk partition > 90%
# Services to monitor (space-separated)
readonly SERVICES="nginx mysql ssh cron"
# Report settings
readonly REPORT_DIR="${HOME}/scripts/health-monitor/reports"
readonly LOG_FILE="/var/log/health_monitor.log"
readonly KEEP_REPORTS=30 # Keep last N reports
# Alert settings (set ALERT_EMAIL to enable email alerts)
readonly ALERT_EMAIL="" # e.g., "admin@example.com"#!/usr/bin/env bash
# lib/report.sh — HTML report generation functions
html_header() {
local title="$1"
local hostname
hostname=$(hostname)
cat <<HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title} — ${hostname}</title>
<style>
body { font-family: -apple-system, sans-serif; background: #0d1117; color: #e6edf3; margin: 0; padding: 20px; }
h1 { color: #58a6ff; border-bottom: 1px solid #30363d; padding-bottom: 10px; }
h2 { color: #79c0ff; margin-top: 30px; }
.card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; margin: 10px 0; }
.ok { color: #3fb950; }
.warning { color: #d29922; }
.critical{ color: #f85149; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid #21262d; }
th { color: #8b949e; font-size: 0.9em; }
.bar-bg { background: #21262d; border-radius: 4px; height: 8px; }
.bar { height: 8px; border-radius: 4px; }
</style>
</head>
<body>
<h1>🖥️ System Health Report</h1>
<p style="color:#8b949e">Host: <strong style="color:#e6edf3">${hostname}</strong> | Generated: <strong style="color:#e6edf3">$(date '+%Y-%m-%d %H:%M:%S')</strong></p>
HTML
}
html_footer() {
echo "</body></html>"
}
# color_class usage_percent threshold
color_class() {
local usage="$1"
local threshold="$2"
if (( usage >= threshold )); then
echo "critical"
elif (( usage >= threshold - 15 )); then
echo "warning"
else
echo "ok"
fi
}
bar_color() {
local class="$1"
case "$class" in
ok) echo "#3fb950" ;;
warning) echo "#d29922" ;;
critical) echo "#f85149" ;;
esac
}#!/usr/bin/env bash
# =============================================================================
# health_monitor.sh — System Health Monitor
# Author: Shell Scripting Course — Capstone Project
# Usage: ./health_monitor.sh [--report-only] [--quiet]
# =============================================================================
set -euo pipefail
IFS=$'\n\t'
# ── Paths ────────────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/config.sh"
source "${SCRIPT_DIR}/lib/report.sh"
# ── Flags ────────────────────────────────────────────────────────────────────
QUIET=false
REPORT_ONLY=false
while [[ $# -gt 0 ]]; do
case "$1" in
--quiet) QUIET=true ;;
--report-only) REPORT_ONLY=true ;;
--help) echo "Usage: $0 [--quiet] [--report-only]"; exit 0 ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
shift
done
# ── Logging ──────────────────────────────────────────────────────────────────
log() {
local level="$1"; shift
local msg="$*"
local ts; ts="$(date '+%Y-%m-%d %H:%M:%S')"
$QUIET || printf "[%s] [%-8s] %s\n" "$ts" "$level" "$msg"
printf "[%s] [%-8s] %s\n" "$ts" "$level" "$msg" >> "$LOG_FILE"
}
log_info() { log "INFO" "$@"; }
log_warn() { log "WARNING" "$@"; }
log_error() { log "ERROR" "$@" >&2; }
# ── Setup ────────────────────────────────────────────────────────────────────
mkdir -p "$REPORT_DIR"
REPORT_FILE="${REPORT_DIR}/health_$(date '+%Y%m%d_%H%M%S').html"
ALERTS=()
log_info "Starting health check on $(hostname)"
# ── Metrics Collection ───────────────────────────────────────────────────────
get_cpu_usage() {
# Get idle percentage, subtract from 100
local idle
idle=$(top -bn1 | grep "Cpu(s)" | awk '{print $8}' | tr -d '%')
echo $(( 100 - ${idle%.*} ))
}
get_mem_usage() {
free | awk '/^Mem:/ {printf "%.0f", $3/$2*100}'
}
get_mem_details() {
free -h | awk '/^Mem:/ {printf "%s used of %s total", $3, $2}'
}
get_disk_sections() {
# Returns lines of: usage% mountpoint device
df -h --output=pcent,target,source | tail -n +2 | grep -v "tmpfs\|udev" | \
sed 's/%//' | awk '{print $1, $2, $3}'
}
check_service() {
local svc="$1"
if systemctl is-active --quiet "$svc" 2>/dev/null; then
echo "running"
else
echo "stopped"
fi
}
# ── Report Building ──────────────────────────────────────────────────────────
build_report() {
html_header "System Health Report" > "$REPORT_FILE"
# ── CPU ──
local cpu_usage
cpu_usage=$(get_cpu_usage)
local cpu_class; cpu_class=$(color_class "$cpu_usage" "$CPU_THRESHOLD")
local cpu_color; cpu_color=$(bar_color "$cpu_class")
log_info "CPU Usage: ${cpu_usage}%"
(( cpu_usage >= CPU_THRESHOLD )) && {
ALERTS+=("⚠️ CPU usage critical: ${cpu_usage}%")
log_warn "CPU at ${cpu_usage}% exceeds threshold of ${CPU_THRESHOLD}%"
}
cat <<HTML >> "$REPORT_FILE"
<h2>⚙️ CPU</h2>
<div class="card">
<p>Usage: <strong class="${cpu_class}">${cpu_usage}%</strong></p>
<div class="bar-bg"><div class="bar" style="width:${cpu_usage}%;background:${cpu_color}"></div></div>
<p style="color:#8b949e;font-size:0.9em;margin-top:8px">Threshold: ${CPU_THRESHOLD}%</p>
</div>
HTML
# ── Memory ──
local mem_usage; mem_usage=$(get_mem_usage)
local mem_details; mem_details=$(get_mem_details)
local mem_class; mem_class=$(color_class "$mem_usage" "$MEM_THRESHOLD")
local mem_color; mem_color=$(bar_color "$mem_class")
log_info "Memory Usage: ${mem_usage}% (${mem_details})"
(( mem_usage >= MEM_THRESHOLD )) && {
ALERTS+=("⚠️ Memory usage critical: ${mem_usage}%")
log_warn "Memory at ${mem_usage}% exceeds threshold of ${MEM_THRESHOLD}%"
}
cat <<HTML >> "$REPORT_FILE"
<h2>💾 Memory</h2>
<div class="card">
<p>Usage: <strong class="${mem_class}">${mem_usage}%</strong> — ${mem_details}</p>
<div class="bar-bg"><div class="bar" style="width:${mem_usage}%;background:${mem_color}"></div></div>
<p style="color:#8b949e;font-size:0.9em;margin-top:8px">Threshold: ${MEM_THRESHOLD}%</p>
</div>
HTML
# ── Disk ──
echo "<h2>🗄️ Disk</h2><div class=\"card\"><table><tr><th>Mount</th><th>Device</th><th>Usage</th><th>Bar</th></tr>" >> "$REPORT_FILE"
while IFS=' ' read -r usage mountpoint device; do
local disk_class; disk_class=$(color_class "$usage" "$DISK_THRESHOLD")
local disk_color; disk_color=$(bar_color "$disk_class")
log_info "Disk $mountpoint: ${usage}%"
(( usage >= DISK_THRESHOLD )) && {
ALERTS+=("⚠️ Disk ${mountpoint} critical: ${usage}%")
log_warn "Disk $mountpoint at ${usage}% exceeds threshold of ${DISK_THRESHOLD}%"
}
cat <<HTML >> "$REPORT_FILE"
<tr>
<td>${mountpoint}</td>
<td style="color:#8b949e">${device}</td>
<td><strong class="${disk_class}">${usage}%</strong></td>
<td style="width:200px"><div class="bar-bg"><div class="bar" style="width:${usage}%;background:${disk_color}"></div></div></td>
</tr>
HTML
done < <(get_disk_sections)
echo "</table></div>" >> "$REPORT_FILE"
# ── Services ──
echo "<h2>🔧 Services</h2><div class=\"card\"><table><tr><th>Service</th><th>Status</th></tr>" >> "$REPORT_FILE"
for svc in $SERVICES; do
local status; status=$(check_service "$svc")
local svc_class="ok"
[[ "$status" == "stopped" ]] && {
svc_class="critical"
ALERTS+=("🚨 Service DOWN: ${svc}")
log_warn "Service $svc is STOPPED"
}
echo "<tr><td>${svc}</td><td><strong class=\"${svc_class}\">${status}</strong></td></tr>" >> "$REPORT_FILE"
done
echo "</table></div>" >> "$REPORT_FILE"
# ── Alert Summary ──
if [[ ${#ALERTS[@]} -gt 0 ]]; then
echo "<h2>🚨 Alert Summary</h2><div class=\"card critical\">" >> "$REPORT_FILE"
for alert in "${ALERTS[@]}"; do
echo "<p>${alert}</p>" >> "$REPORT_FILE"
done
echo "</div>" >> "$REPORT_FILE"
else
echo "<h2>✅ All Systems Normal</h2><div class=\"card ok\"><p>No thresholds exceeded.</p></div>" >> "$REPORT_FILE"
fi
html_footer >> "$REPORT_FILE"
}
# ── Alert Dispatch ───────────────────────────────────────────────────────────
send_alerts() {
[[ ${#ALERTS[@]} -eq 0 ]] && return
[[ -z "$ALERT_EMAIL" ]] && return
local subject="[ALERT] $(hostname) Health Monitor — $(date '+%Y-%m-%d %H:%M')"
local body
printf -v body "System Health Alerts:\n\n%s\n\nFull report: %s" \
"$(printf '%s\n' "${ALERTS[@]}")" "$REPORT_FILE"
echo "$body" | mail -s "$subject" "$ALERT_EMAIL"
log_info "Alert email sent to $ALERT_EMAIL"
}
# ── Cleanup Old Reports ───────────────────────────────────────────────────────
cleanup_old_reports() {
local count
count=$(ls -1 "${REPORT_DIR}"/health_*.html 2>/dev/null | wc -l)
if (( count > KEEP_REPORTS )); then
ls -1t "${REPORT_DIR}"/health_*.html | tail -n +"$(( KEEP_REPORTS + 1 ))" | xargs rm -f
log_info "Cleaned up old reports (kept last $KEEP_REPORTS)"
fi
}
# ── Main ─────────────────────────────────────────────────────────────────────
main() {
build_report
send_alerts
cleanup_old_reports
log_info "Report saved: $REPORT_FILE"
echo "$REPORT_FILE"
}
main "$@"# 1. Create the project structure
mkdir -p ~/scripts/health-monitor/lib
mkdir -p ~/scripts/health-monitor/reports
# 2. Create and edit all three files as shown above, then:
chmod +x ~/scripts/health-monitor/health_monitor.sh
# 3. Test it manually
~/scripts/health-monitor/health_monitor.sh
# 4. Open the generated report in a browser
xdg-open "$(~/scripts/health-monitor/health_monitor.sh --quiet)"
# 5. Schedule with cron — every 15 minutes
crontab -e
# Add:
# */15 * * * * /home/akash278/scripts/health-monitor/health_monitor.sh --quiet| Concept Learned | Where It's Used |
|---|---|
Shebang + set -euo pipefail |
Line 1–2 of main script |
Variables, readonly, defaults |
config.sh |
Functions + local + return |
All lib/report.sh and collectors |
while/case loops |
Arg parsing, service loop, disk loop |
getopts-style flag parsing |
--quiet, --report-only flags |
Arrays + += |
ALERTS array |
| String operations + heredocs | HTML generation |
Process substitution < <() |
Disk section parsing |
trap EXIT |
Implicit via set -e + structured logging |
| Cron scheduling | Deployment step |
| Security: quoting, validation | Throughout |
🎓 Congratulations! You have completed the Shell Scripting Professional Course. You now have the foundational knowledge to write production-quality Bash scripts for automation, DevOps, system administration, and beyond.