Build a Server Health Monitor with Bash
Write a bash script that monitors CPU, memory, disk, and processes.
Introduction
30 Second Summary
Every computer is like a car. Without regular check-ups, small problems build up until something breaks down at the worst possible time. System administrators use monitoring tools to catch issues early, but those tools are just scripts that know where to look.
In this project, you will build a command-line system health monitor using nothing but the Linux terminal. Your tool will check disk space, memory, processes, and logs, then produce a formatted report that flags anything unusual.
What You'll Build
A Bash script that monitors CPU, memory, disk, and processes, then generates timestamped reports with warnings for anything that exceeds your thresholds.
By the end of this project, you'll have:
- A multi-function bash script (health-monitor.sh) that generates formatted system health reports covering disk, memory, processes, and logs.
- Hands-on experience with 30+ Linux commands across file management, text processing, permissions, system monitoring, log reading, and archiving.
- A cron job that automatically runs your health check every 5 minutes and saves timestamped reports.
- Secret Mission: Add network connectivity checks that ping a list of servers and flag any that are unreachable.
Are there any prerequisites?
You need WSL2 with Ubuntu already installed on your Windows machine. Everything else (nano, cron, bash) comes pre-installed with Ubuntu.
Get Oriented in the Terminal
Everything you build in this project happens inside the Linux terminal.
Before writing any scripts, you need to know where you are, who you are, and how to move around the filesystem. In this step, you'll open your WSL terminal and create the project folder structure that your health monitor will live in.
In this step, get ready to:
- Open your WSL Ubuntu terminal and explore basic commands.
- Create the project directory structure for your health monitor.
- Learn to read the terminal prompt and navigate the filesystem.
Open WSL and explore your environment
Your Ubuntu terminal is where every command in this project runs. Let's open it and find out who you are in the system.
- Press the Windows key to open your search bar.
- Type Ubuntu and press Enter to open your WSL terminal.
You should see a dark terminal window with a blinking cursor. This is your Linux environment running inside Windows.
- Find out who you are and where you are by running these commands one at a time:
whoami
hostname
pwd
What do these commands do?
- whoami prints your Linux username.
- hostname shows the name of your machine.
- pwd stands for "print working directory." It tells you which folder you're currently sitting in.
- See what files and folders exist in your current location by running:
ls
You might see some default folders like Desktop, Documents, or nothing at all if this is a fresh Ubuntu install.
What is ls?
ls stands for "list." It shows you the contents of whichever directory you're currently in. Think of it as opening a folder on your desktop to peek inside.
Create the project structure
Every project needs a home. You'll create a dedicated folder for your health monitor, plus a subfolder where your reports will be saved.
- Create your project directory and a nested reports folder by running this command:
mkdir -p ~/health-monitor/reports
What does this command do?
- mkdir stands for "make directory." It creates a new folder.
- -p tells it to create parent directories as needed. Without this flag, you'd get an error because health-monitor doesn't exist yet.
- ~/health-monitor/reports is the path. The ~ means your home directory.
- Confirm the directories were created by running:
ls ~/health-monitor
You should see reports listed in the output.
Don't see the reports folder?
- Check for typos in the path. The command is case-sensitive, so Health-Monitor is not the same as health-monitor.
- Make sure you included the -p flag. Without it, mkdir cannot create nested directories in one go.
Help me troubleshoot my mkdir command
- Move into your new project folder by running:
cd ~/health-monitor
- View detailed information about the current directory by running:
ls -la
You'll see the reports folder listed along with . (current directory) and .. (parent directory).
What does the -la flag mean?
-l gives you a "long" listing with permissions, owner, size, and date. -a shows hidden files (those starting with a dot). Combined as -la, you get the full picture of everything in the folder.
- Create a placeholder file to practice the touch command:
touch notes.txt
- Confirm the file was created by running:
ls
You should see both notes.txt and reports in the output.
What does touch do?
touch creates an empty file if it doesn't already exist. If the file already exists, it updates the file's timestamp without changing its contents.
Learn to read the terminal prompt
Every time you see the blinking cursor, the text to its left tells you exactly who you are and where you are. Understanding this prompt saves you from getting lost.
What does the prompt mean?
Your prompt looks something like user@host:path$. Here's what each part means:
- user is your Linux username (what whoami showed you).
- host is your machine name (what hostname showed you).
- path is your current directory. A ~ means you're in your home folder.
- $ means you're a regular user (not the root administrator).
Now let's practice moving between directories so you always know how to get back.
- Jump to your home directory by running:
cd ~
- Jump to the root of the entire filesystem by running:
cd /
- Go up one level (to the parent directory) by running:
cd ..
Three ways to navigate
- cd ~ always takes you home, no matter where you are.
- cd / takes you to the very top of the filesystem (the root). Everything on your system lives somewhere below /.
- cd .. moves you one level up. If you're in ~/health-monitor/reports, it takes you to ~/health-monitor.
- Clear the clutter from your screen by running:
clear
Your terminal is now blank with a fresh prompt at the top. The clear command doesn't delete anything. It just scrolls old output off the screen.
- Navigate back to your project folder by running:
cd ~/health-monitor
Check your prompt. It should now show ~/health-monitor as your current path.
Always check your prompt
Before running any command, glance at the path in your prompt to make sure you're in the right directory. Most errors in Linux come from running commands in the wrong location.
Your project folder is ready and you know how to move around the filesystem. Next up, you'll explore the system information commands that will power your health monitor script.
Explore System Information Commands
You now have a project directory set up and you know how to move around the filesystem. The next step is learning how to ask your system questions about its health.
A system administrator needs to quickly check disk space, memory usage, and what processes are consuming resources. In this step, you'll run the core commands that gather this information.
In this step, get ready to:
- Check disk and memory usage with human-readable output.
- View running processes and identify top resource consumers.
- Read CPU load averages and understand what they mean.
Check disk and memory usage
Disk space and memory are two of the most common things that cause problems on a server. A full disk can crash applications. Low memory can freeze your system.
- Make sure you're in the project folder by running:
cd ~/health-monitor
- Check how much disk space is used on your system by running:
df -h
What does this output mean?
The -h flag stands for "human-readable". Instead of raw bytes, you see sizes in GB and MB.
Each row is a filesystem. The columns to focus on are Size (total capacity), Used (how much is consumed), Use% (the percentage full), and Mounted on (where it lives in the directory tree).
- Check the size of each item in your current directory by running:
du -sh *
You should see the reports directory listed with its size. This command is useful for finding which folders are eating up your disk.
- Check your system's memory usage by running:
free -h
What does this output mean?
The Mem: row shows your physical RAM. total is how much you have, used is what's currently consumed, and available is how much is free for new processes.
The Swap: row shows disk space being used as overflow memory. High swap usage means your system is running low on actual RAM.
View running processes and system uptime
Knowing what's running on your machine is essential for troubleshooting. A single runaway process can consume all your CPU or memory.
- View all running processes on your system by running:
ps aux
What do the columns mean?
- USER is who owns the process.
- PID is the process ID (a unique number for each running process).
- %CPU and %MEM show how much of each resource the process is using.
- COMMAND is the program that started the process.
The full list is long. In practice, you want to zero in on the processes consuming the most resources.
- Show only the top memory-consuming processes by running:
ps aux --sort=-%mem | head
What does this command do?
The --sort=-%mem flag sorts processes by memory usage in descending order (the - means highest first). The pipe | sends that sorted list to head, which shows only the first 10 lines.
- Check how long your system has been running by running:
uptime
You should see a single line showing the current time, how long the system has been up, the number of users, and three load average numbers.
Understand CPU load averages
The three numbers at the end of the uptime output are your system's load averages. They tell you how busy your CPU has been over three different time windows.
What are load averages?
The three numbers represent the average number of processes waiting for CPU time over the last 1 minute, 5 minutes, and 15 minutes.
On a single-core machine, a load of 1.0 means the CPU is fully occupied. On a 4-core machine, a load of 4.0 means full utilization. If the 1-minute average is much higher than the 15-minute average, your system is experiencing a recent spike.
- Run uptime once more and look at the three load average numbers at the end of the line.
- Compare the three values. If they're all similar, your system load has been steady. If the first number is much larger, something recently started consuming resources.
Quick reference for this step
- df -h shows disk usage per filesystem.
- du -sh * shows sizes of items in the current directory.
- free -h shows RAM and swap usage.
- ps aux --sort=-%mem | head shows top memory consumers.
- uptime shows how long the system has been running and CPU load averages.
You now know how to check your system's vital signs from the command line. Next up, you'll learn how to filter and format this output so you can extract exactly the values you need.
Process and Format Text Output
In Step 2, you ran commands like df -h and free -h and saw walls of text fill the terminal. A health monitor needs specific values from that output, not everything at once.
Linux provides text processing tools that filter, extract, and reshape command output. In this step, you'll use grep, awk, and pipes to pull exactly the data you need from system commands.
In this step, get ready to:
- Filter command output with grep to isolate specific lines.
- Extract individual values with awk to pull specific columns.
- Chain commands with pipes to sort and count results.
Filter output with grep
The grep command searches through text and returns only lines that match a pattern. Instead of reading all the output from a command, you can pipe it into grep to find the one line you care about.
- Filter the df -h output to show only the root partition by running this command:
df -h | grep "/$"
What does this command do?
The pipe (|) sends the output of df -h directly into grep as input.
The pattern "/$" matches lines ending with a single /. That's the root filesystem mount point. You should see one line showing your root partition's size, used space, and available space.
You should see a single line of output showing your root partition's disk usage.
- Now try filtering for lines that do NOT contain a pattern. Run this command to exclude tmpfs lines from disk output:
df -h | grep -v "tmpfs"
What does the -v flag do?
The -v flag inverts the match. Instead of showing lines that contain tmpfs, it shows every line that does NOT contain it. This is useful for removing noise from output.
Seeing no output from grep?
If your grep returns nothing, the pattern might not match exactly. Patterns are case-sensitive by default. Double-check you typed /$ with a forward slash, not a backslash.
Help me debug my grep command
Extract specific columns with awk
Sometimes you need a single number from a line of output, not the whole line. The awk command splits each line into columns and lets you print only the ones you want.
- Extract just the amount of used memory in megabytes by running this command:
free -m | awk '/Mem:/ {print $3}'
What does this awk command do?
This command has two parts working together. First, free -m displays memory in megabytes. The pipe sends that output into awk.
Inside awk, /Mem:/ matches only the line containing "Mem:". Then {print $3} prints the third column of that line, which is the "used" memory value. You should see a single number.
You should see a single number representing your used memory in megabytes.
- Now extract the total memory value by changing the column number. Run this command:
free -m | awk '/Mem:/ {print $2}'
How awk numbers columns
In awk, columns are numbered starting from $1. For the free -m output, $1 is the row label ("Mem:"), $2 is total memory, $3 is used memory, and $4 is free memory.
Getting an empty result or an error?
Make sure the single quotes around the awk program are straight quotes ('), not curly quotes. Also check that /Mem:/ has a capital M. Awk patterns are case-sensitive.
Help me troubleshoot my awk command
Combine commands with pipes to sort and count
Pipes let you chain as many commands together as you need. Each command transforms the output before passing it to the next one.
- Sort all running processes by memory usage and show only the top entries by running this command:
ps aux --sort=-%mem | head
What does this command do?
The --sort=-%mem flag tells ps to sort processes by memory usage in descending order (the - means descending). The head command then takes only the first 10 lines, giving you the top memory consumers.
You should see a table of the processes using the most memory on your system.
- Count how many processes are currently running by piping ps aux into wc -l:
ps aux | wc -l
What does wc -l do?
The wc command counts words, lines, or characters. The -l flag counts lines only. Since each process is one line in ps aux output, the number you see is your total process count.
- Try one more combination. Sort processes by CPU usage in descending order and show the top 5 by running:
ps aux --sort=-%cpu | head -6
Why head -6 instead of head -5?
The ps aux output includes a header row (USER, PID, %CPU, etc.). Using head -6 keeps that header plus 5 actual process lines, so you can still read the column names.
Pipes are the glue of Linux
Every command you've used in this step (grep, awk, sort, head, wc) does one small thing well. Pipes let you snap them together like building blocks to create powerful data pipelines.
You now have the building blocks to extract any value from any command's output. Next up, you'll combine these techniques into a real bash script that generates a full health report.
Write the Health Check Script
You have explored system commands individually and piped their outputs through text processing tools. Now it is time to combine everything into a single reusable script.
In this step, you will create a bash script called health-monitor.sh that checks disk usage, memory consumption, and top processes. The script uses functions and conditionals to flag anything above a threshold you define.
In this step, get ready to:
- Create the script file and set up configuration variables.
- Write functions that check disk and memory usage.
- Write a function for process monitoring and assemble the full report.
Create the script file and add configuration
Every bash script starts with a shebang line that tells Linux which interpreter to use. After that, you will define configuration variables so the rest of the script can reference them.
- Open a new file in nano by running this command:
nano health-monitor.sh
What is nano?
nano is a beginner-friendly text editor that runs inside the terminal. You will see keyboard shortcuts listed at the bottom of the screen.
If the file does not exist yet, nano creates it for you when you save.
You should now see an empty nano editor screen with a cursor at the top.
- Type the following code into nano to set up the script header and configuration:
#!/bin/bash
# =============================================================
# System Health Monitor
# Generates a formatted health report with warnings
# =============================================================
# Configuration
REPORT_DIR="$HOME/health-monitor/reports"
DISK_THRESHOLD=80
MEM_THRESHOLD=80
REPORT_FILE="$REPORT_DIR/health-$(date +%F-%H%M).log"
# Ensure reports directory exists
mkdir -p "$REPORT_DIR"
What does this code do?
- #!/bin/bash is the shebang line. It tells Linux to run this file with the bash interpreter.
- REPORT_DIR stores the path where reports will be saved.
- DISK_THRESHOLD and MEM_THRESHOLD define the percentage limits that trigger a warning.
- REPORT_FILE uses $(date +%F-%H%M) to generate a unique filename with today's date and time.
- mkdir -p ensures the reports directory exists before the script tries to write to it.
Write the disk check function
The disk check function loops through each filesystem, extracts the usage percentage, and prints a warning if it exceeds your threshold. This combines df, awk, and sed in one reusable block.
- Below the mkdir -p line, add the following function:
# ----- Functions -----
check_disk() {
echo "==== Disk Usage ===="
df -h | grep -v "tmpfs" | grep -v "Filesystem"
echo ""
echo "-- Disk Warnings --"
local warnings=0
while read -r line; do
usage=$(echo "$line" | awk '{print $5}' | sed 's/%//')
mount=$(echo "$line" | awk '{print $6}')
if [ "$usage" -gt "$DISK_THRESHOLD" ] 2>/dev/null; then
echo "[WARNING] $mount is at ${usage}% (threshold: ${DISK_THRESHOLD}%)"
warnings=$((warnings + 1))
fi
done < <(df -h | grep -v "tmpfs" | grep -v "Filesystem")
if [ "$warnings" -eq 0 ]; then
echo "All filesystems within normal limits."
fi
echo ""
}
What does this code do?
- grep -v "tmpfs" filters out temporary filesystems that clutter the output.
- The while read -r line loop processes each line of disk output one at a time.
- awk '{print $5}' grabs the fifth column (the usage percentage) and sed 's/%//' strips the % sign so you can compare it as a number.
- The if [ "$usage" -gt "$DISK_THRESHOLD" ] conditional checks whether usage exceeds your threshold and prints a warning if so.
- local warnings=0 keeps a counter so the script can report "all clear" when nothing is over the limit.
Write the memory and process functions
The memory function calculates what percentage of RAM is in use and compares it against your threshold. The process function simply lists the top resource consumers so you can spot anything unusual.
- Below the check_disk() function, add the memory check function:
check_memory() {
echo "==== Memory Usage ===="
free -h
echo ""
local mem_total
local mem_used
local mem_percent
mem_total=$(free -m | awk '/Mem:/ {print $2}')
mem_used=$(free -m | awk '/Mem:/ {print $3}')
mem_percent=$((mem_used * 100 / mem_total))
echo "Memory utilization: ${mem_percent}%"
if [ "$mem_percent" -gt "$MEM_THRESHOLD" ]; then
echo "[WARNING] Memory usage exceeds threshold (${MEM_THRESHOLD}%)"
else
echo "Memory within normal limits."
fi
echo ""
}
What does this code do?
- free -m outputs memory in megabytes, which makes the arithmetic simpler.
- awk '/Mem:/ {print $2}' finds the line starting with Mem: and extracts the total memory column.
- The formula mem_used * 100 / mem_total gives you the percentage of memory in use.
- The conditional prints a warning or an "all clear" message depending on whether the percentage exceeds MEM_THRESHOLD.
- Below check_memory(), add the process check function:
check_processes() {
echo "==== Top 5 Processes by Memory ===="
ps aux --sort=-%mem | head -6
echo ""
echo "==== Top 5 Processes by CPU ===="
ps aux --sort=-%cpu | head -6
echo ""
}
Why head -6 instead of head -5?
ps aux always outputs a header row (USER, PID, %CPU, etc.) as the first line. Using head -6 keeps that header plus the top 5 actual processes.
- Below check_processes(), add the main report block that calls all three functions and writes to the report file:
# ----- Main Report -----
{
echo "============================================"
echo " System Health Report"
echo " Host: $(hostname)"
echo " Date: $(date)"
echo " Uptime: $(uptime -p)"
echo "============================================"
echo ""
check_disk
check_memory
check_processes
echo "==== System Load ===="
uptime | awk -F'load average:' '{print "Load Average:" $2}'
echo ""
echo "============================================"
echo " End of Report"
echo "============================================"
} > "$REPORT_FILE"
echo "Report saved to: $REPORT_FILE"
What does the main report block do?
The curly braces { ... } group all the echo statements and function calls into a single output stream. The > "$REPORT_FILE" at the end redirects that entire stream into the timestamped log file.
The final echo line sits outside the braces so it prints to your terminal instead of the file. This gives you confirmation that the report was saved.
- Save the file by pressing Ctrl+O, then press Enter to confirm the filename.
- Exit nano by pressing Ctrl+X.
- Verify the file was created by running:
ls -la health-monitor.sh
You should see health-monitor.sh listed with its file size and timestamp.
Don't see the file?
- Make sure you pressed Ctrl+O (the letter O, not zero) to save before exiting.
- Confirm you are in the ~/health-monitor/ directory by running pwd.
- If you accidentally exited without saving, run nano health-monitor.sh again and retype the code.
help me find my health-monitor.sh file
✔️ Awesome, I've got everything!
Great. Double check you have saved your file and exited nano.
ⓧ I'd like to double check the full code
Here is the complete health-monitor.sh file you should have at this point:
#!/bin/bash
# =============================================================
# System Health Monitor
# Generates a formatted health report with warnings
# =============================================================
# Configuration
REPORT_DIR="$HOME/health-monitor/reports"
DISK_THRESHOLD=80
MEM_THRESHOLD=80
REPORT_FILE="$REPORT_DIR/health-$(date +%F-%H%M).log"
# Ensure reports directory exists
mkdir -p "$REPORT_DIR"
# ----- Functions -----
check_disk() {
echo "==== Disk Usage ===="
df -h | grep -v "tmpfs" | grep -v "Filesystem"
echo ""
echo "-- Disk Warnings --"
local warnings=0
while read -r line; do
usage=$(echo "$line" | awk '{print $5}' | sed 's/%//')
mount=$(echo "$line" | awk '{print $6}')
if [ "$usage" -gt "$DISK_THRESHOLD" ] 2>/dev/null; then
echo "[WARNING] $mount is at ${usage}% (threshold: ${DISK_THRESHOLD}%)"
warnings=$((warnings + 1))
fi
done < <(df -h | grep -v "tmpfs" | grep -v "Filesystem")
if [ "$warnings" -eq 0 ]; then
echo "All filesystems within normal limits."
fi
echo ""
}
check_memory() {
echo "==== Memory Usage ===="
free -h
echo ""
local mem_total
local mem_used
local mem_percent
mem_total=$(free -m | awk '/Mem:/ {print $2}')
mem_used=$(free -m | awk '/Mem:/ {print $3}')
mem_percent=$((mem_used * 100 / mem_total))
echo "Memory utilization: ${mem_percent}%"
if [ "$mem_percent" -gt "$MEM_THRESHOLD" ]; then
echo "[WARNING] Memory usage exceeds threshold (${MEM_THRESHOLD}%)"
else
echo "Memory within normal limits."
fi
echo ""
}
check_processes() {
echo "==== Top 5 Processes by Memory ===="
ps aux --sort=-%mem | head -6
echo ""
echo "==== Top 5 Processes by CPU ===="
ps aux --sort=-%cpu | head -6
echo ""
}
# ----- Main Report -----
{
echo "============================================"
echo " System Health Report"
echo " Host: $(hostname)"
echo " Date: $(date)"
echo " Uptime: $(uptime -p)"
echo "============================================"
echo ""
check_disk
check_memory
check_processes
echo "==== System Load ===="
uptime | awk -F'load average:' '{print "Load Average:" $2}'
echo ""
echo "============================================"
echo " End of Report"
echo "============================================"
} > "$REPORT_FILE"
echo "Report saved to: $REPORT_FILE"
Your health check script is written and saved. Next up, you will make it executable and run it to see your first real health report.
Add Permissions and Run the Script
Your health monitor script is written, but Linux won't let you run it yet. Every file in Linux has a set of permissions that control who can read, write, or execute it.
In this step, you'll grant execute permission to your script, run it for the first time, and learn how the Linux permission model works by experimenting with different settings.
In this step, get ready to:
- Make health-monitor.sh executable with chmod.
- Run the script and verify the report was created.
- Explore the Linux permission model with different chmod values.
Make the script executable
When you create a new file in Linux, it starts with read and write permissions but no execute permission. If you try to run a script without execute permission, the system will refuse with a "Permission denied" error.
- First, check the current permissions of your script by running:
ls -la health-monitor.sh
You should see output starting with something like -rw-r--r--. Notice there is no x anywhere in the permission string. That means nobody can execute this file yet.
How do I read Linux permissions?
The permission string has 10 characters. The first character is the file type (- for a regular file, d for a directory). The remaining 9 characters are three groups of three.
- Characters 2-4: Owner permissions (the user who owns the file).
- Characters 5-7: Group permissions (users in the file's group).
- Characters 8-10: Other permissions (everyone else).
Each group uses r (read), w (write), and x (execute). A dash (-) means that permission is not granted.
- Add execute permission to your script by running:
chmod +x health-monitor.sh
What does this command do?
The chmod command changes file permissions. The +x flag adds execute permission for the owner, group, and others.
- Verify the permission change by running:
ls -la health-monitor.sh
You should now see -rwxr-xr-x at the start of the line. The x appears in all three groups, meaning anyone can execute this file.
Still seeing no x in the permissions?
Make sure you typed chmod +x health-monitor.sh with a space between chmod and +x, and between +x and the filename. Also confirm you are in the ~/health-monitor/ directory where the script lives.
help me fix my chmod command
Run the script and verify the report
Now that your script has execute permission, you can run it directly from the terminal. The ./ prefix tells Linux to look for the script in the current directory.
- Run your health monitor script by running:
./health-monitor.sh
You should see output like Report saved to: /home/<your-user>/health-monitor/reports/health-2025-05-13-1430.log (the timestamp will match when you ran it).
- Confirm the report file was created by running:
ls -la reports/
You should see a file named something like health-2025-05-13-1430.log listed in the output. The timestamp portion will reflect the time you ran the script.
- View the full report contents by running:
cat reports/health-*.log
You should see a formatted report with sections for Disk Usage, Disk Warnings, Memory Usage, Top 5 Processes by Memory, Top 5 Processes by CPU, and System Load.
Seeing Permission denied when running the script?
This means the execute permission was not applied correctly. Run chmod +x health-monitor.sh again, then try ./health-monitor.sh once more.
If you see a syntax error instead, open the script in nano with nano health-monitor.sh and check that the very first line is exactly #!/bin/bash with no spaces before the #.
help me debug my script execution
Experiment with permission values
The +x shorthand is convenient, but Linux also supports numeric permission codes that give you finer control. Each permission has a number: read is 4, write is 2, and execute is 1. You add them together for each group (owner, group, others).
- Set the script to owner-only access by running:
chmod 700 health-monitor.sh
- Check the permissions by running:
ls -la health-monitor.sh
You should see -rwx------. The owner has full access (7 = 4+2+1 = read+write+execute), but group and others have nothing (0).
- Now set it to a more open permission by running:
chmod 755 health-monitor.sh
- Check the permissions again by running:
ls -la health-monitor.sh
You should see -rwxr-xr-x. The owner has full access (7), and group and others can read and execute but not write (5 = 4+1).
When would I use 700 vs 755?
Use 700 for scripts that contain sensitive information or should only be run by you. Use 755 for scripts that other users on the system might need to execute.
On a shared server, permissions are critical for security. On your personal WSL setup, the distinction is less impactful. But building the habit now means you will be ready when working on production servers.
Your script is executable and generating real health reports. Next up, you'll learn to read and filter system logs to surface errors and warnings in your reports.
Read and Filter System Logs
Your health monitor now checks disk, memory, and processes. But there's another critical source of system intelligence you haven't tapped into yet: logs.
System logs record events happening behind the scenes. Hardware errors, driver warnings, and service failures all get logged automatically. In this step, you'll learn to read and filter these logs, then add a function to your script that surfaces recent problems in every health report.
In this step, get ready to:
- View kernel messages with dmesg and understand what system logs tell you.
- Read systemd journal entries with journalctl or fall back to /var/log/syslog.
- Add a check_logs() function to your script that filters for errors and warnings.
View recent kernel messages with dmesg
The Linux kernel constantly records events as your system runs. Hardware being detected, drivers loading, and errors occurring all get written to a ring buffer that dmesg lets you read.
- View the 20 most recent kernel messages by running this command:
dmesg | tail -20
What does this command do?
dmesg prints the kernel's message buffer. On its own it outputs thousands of lines, so piping to tail -20 trims it to just the last 20.
You'll see messages about hardware, drivers, and system events. Each line starts with a timestamp (in seconds since boot) followed by the message text.
You should see lines describing system events. Some may mention USB devices, network interfaces, or memory allocation.
- Now filter for only errors and warnings by running:
dmesg | grep -i "error\|warning" | tail -5
What does this command do?
grep -i "error\|warning" searches for lines containing either "error" or "warning" (case-insensitive thanks to -i). The \| is the OR operator in grep's basic regex.
Piping to tail -5 limits the output to the 5 most recent matches. This is the exact filtering you'll add to your script shortly.
You might see a few lines of errors or warnings, or nothing at all if your system is healthy. Both results are normal.
Read systemd journal entries
Beyond the kernel buffer, Linux also keeps service-level logs. On systems with systemd enabled, journalctl gives you access to logs from every running service.
- Try reading recent journal entries by running:
journalctl --no-pager -n 20
What does this command do?
--no-pager prints the output directly to your terminal instead of opening an interactive viewer. -n 20 limits the output to the 20 most recent entries.
Seeing "No journal files were found"?
This means systemd is not enabled in your WSL instance. No problem. Try reading the syslog file instead by running:
cat /var/log/syslog | tail -20
If that file also doesn't exist, that's fine too. Your system uses dmesg as the primary log source, which always works in WSL2. The script you'll write next relies on dmesg specifically for this reason.
Help me understand why journalctl doesn't work in my WSL setup.
Why does the script use dmesg instead of journalctl?
dmesg works universally in WSL2 regardless of whether systemd is enabled. journalctl only works when systemd is the init system. For a portable health monitor, dmesg is the safer choice.
Add a check_logs() function to the script
Now you'll combine what you just learned into a function that automatically surfaces recent errors and warnings in every health report.
- Open health-monitor.sh in nano by running:
nano health-monitor.sh
- Scroll down to find the closing } of the check_processes() function. Add the following check_logs() function below it (leave a blank line between the two functions):
check_logs() {
echo "==== Recent Errors/Warnings (dmesg) ===="
local log_output
log_output=$(dmesg 2>/dev/null | grep -i "error\|warning" | tail -5)
if [ -z "$log_output" ]; then
echo "No recent errors or warnings found."
else
echo "$log_output"
fi
echo ""
}
What does this code do?
- dmesg 2>/dev/null reads the kernel buffer and silently discards any permission errors (the 2>/dev/null part redirects error output to nowhere).
- local log_output declares a variable scoped to this function, then stores the filtered results in it.
- if [ -z "$log_output" ] checks if the variable is empty (meaning no errors or warnings were found). If empty, it prints a reassuring message. Otherwise, it prints the actual errors.
- Now scroll down to the main report block. Find this line inside the curly braces:
check_processes
- Add a call to check_logs on the line directly below check_processes, so it looks like this:
check_processes
check_logs
- Save the file by pressing Ctrl+O, then Enter to confirm, then Ctrl+X to exit nano.
- Run the script to generate a fresh report by running:
./health-monitor.sh
- You should see the "Report saved to:" message with a new timestamp.
- View the new report to confirm the logs section is included by running:
cat $(ls -t reports/*.log | head -1)
You should see a new section titled "==== Recent Errors/Warnings (dmesg) ====" in your report. It will either show recent error lines or the message "No recent errors or warnings found."
Don't see the logs section in your report?
- Make sure check_logs is called inside the curly braces { } of the main report block, not outside them.
- Check that the function definition check_logs() appears above the main report block. Bash reads top-to-bottom, so a function must be defined before it's called.
- Verify you didn't accidentally delete the closing } of check_processes() when adding the new function below it.
Help me figure out why check_logs isn't appearing in my health report.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's the complete health-monitor.sh file at this point:
#!/bin/bash
# =============================================================
# System Health Monitor
# Generates a formatted health report with warnings
# =============================================================
# Configuration
REPORT_DIR="$HOME/health-monitor/reports"
DISK_THRESHOLD=80
MEM_THRESHOLD=80
REPORT_FILE="$REPORT_DIR/health-$(date +%F-%H%M).log"
# Ensure reports directory exists
mkdir -p "$REPORT_DIR"
# ----- Functions -----
check_disk() {
echo "==== Disk Usage ===="
df -h | grep -v "tmpfs" | grep -v "Filesystem"
echo ""
echo "-- Disk Warnings --"
local warnings=0
while read -r line; do
usage=$(echo "$line" | awk '{print $5}' | sed 's/%//')
mount=$(echo "$line" | awk '{print $6}')
if [ "$usage" -gt "$DISK_THRESHOLD" ] 2>/dev/null; then
echo "[WARNING] $mount is at ${usage}% (threshold: ${DISK_THRESHOLD}%)"
warnings=$((warnings + 1))
fi
done < <(df -h | grep -v "tmpfs" | grep -v "Filesystem")
if [ "$warnings" -eq 0 ]; then
echo "All filesystems within normal limits."
fi
echo ""
}
check_memory() {
echo "==== Memory Usage ===="
free -h
echo ""
local mem_total
local mem_used
local mem_percent
mem_total=$(free -m | awk '/Mem:/ {print $2}')
mem_used=$(free -m | awk '/Mem:/ {print $3}')
mem_percent=$((mem_used * 100 / mem_total))
echo "Memory utilization: ${mem_percent}%"
if [ "$mem_percent" -gt "$MEM_THRESHOLD" ]; then
echo "[WARNING] Memory usage exceeds threshold (${MEM_THRESHOLD}%)"
else
echo "Memory within normal limits."
fi
echo ""
}
check_processes() {
echo "==== Top 5 Processes by Memory ===="
ps aux --sort=-%mem | head -6
echo ""
echo "==== Top 5 Processes by CPU ===="
ps aux --sort=-%cpu | head -6
echo ""
}
check_logs() {
echo "==== Recent Errors/Warnings (dmesg) ===="
local log_output
log_output=$(dmesg 2>/dev/null | grep -i "error\|warning" | tail -5)
if [ -z "$log_output" ]; then
echo "No recent errors or warnings found."
else
echo "$log_output"
fi
echo ""
}
# ----- Main Report -----
{
echo "============================================"
echo " System Health Report"
echo " Host: $(hostname)"
echo " Date: $(date)"
echo " Uptime: $(uptime -p)"
echo "============================================"
echo ""
check_disk
check_memory
check_processes
check_logs
echo "==== System Load ===="
uptime | awk -F'load average:' '{print "Load Average:" $2}'
echo ""
echo "============================================"
echo " End of Report"
echo "============================================"
} > "$REPORT_FILE"
echo "Report saved to: $REPORT_FILE"
Your health monitor now checks five dimensions of system health: disk, memory, processes, logs, and load averages. Next up, you'll learn to archive old reports so they don't pile up forever.
Archive Old Reports
Your health monitor now generates reports and checks system logs for issues. Over time, those report files will pile up in your reports/ directory.
In this step, you will learn how to compress files into archives using tar. Then you will add an archiving function to your script so old reports get cleaned up automatically.
In this step, get ready to:
- Create sample report files to practice archiving.
- Compress files with tar and verify the archive contents.
- Add an archive_reports() function to your script.
Create sample old report files
To practice archiving, you need a few report files to work with. A for loop lets you create multiple files in one command instead of typing touch three separate times.
- Create three sample report files by running this command:
for i in 1 2 3; do touch reports/health-2025-05-0${i}-0900.log; done
What does this command do?
The for i in 1 2 3 part loops through the numbers 1, 2, and 3. Each time through the loop, ${i} gets replaced with the current number.
So touch creates health-2025-05-01-0900.log, then health-2025-05-02-0900.log, then health-2025-05-03-0900.log.
- Verify the files were created by running:
ls reports/
You should see your three new files alongside your existing report from earlier.
Don't see the files?
Make sure you are in the ~/health-monitor/ directory. Run pwd to check. If you are somewhere else, run cd ~/health-monitor first.
help me figure out why my for loop didn't create files in reports/
Compress files with tar
The tar command bundles multiple files into a single compressed archive. This is how Linux administrators package log files for storage or transfer.
- Compress all .log files in the reports directory by running:
tar -czvf reports-archive.tar.gz reports/*.log
What do the tar flags mean?
- -c means create a new archive.
- -z means compress with gzip.
- -v means verbose (print each file as it gets added).
- -f means the next argument is the archive filename.
You should see each file name printed as tar adds it to the archive.
- Verify the archive contents by listing what is inside it:
tar -tzvf reports-archive.tar.gz
You should see a list of all the .log files with their sizes and timestamps. The -t flag lists the contents without extracting them.
Seeing 'No such file or directory'?
Make sure you are in ~/health-monitor/ (not inside the reports/ subdirectory). The command expects the path reports/*.log relative to your current location.
help me troubleshoot why tar can't find my report files
Add an archive function to the script
Now that you know how tar works, you can automate the archiving process. You will add a function that finds reports older than one day, compresses them, and removes the originals.
- Open health-monitor.sh in nano by running:
nano health-monitor.sh
- Scroll down to the # ----- Functions ----- section. Find the closing brace of the check_logs() function.
- Add the following archive_reports() function below check_logs():
archive_reports() {
local old_reports
old_reports=$(find "$REPORT_DIR" -name "*.log" -mtime +1 2>/dev/null)
if [ -n "$old_reports" ]; then
tar -czvf "$REPORT_DIR/archive-$(date +%F).tar.gz" $old_reports 2>/dev/null
rm -f $old_reports
echo "Archived old reports."
fi
}
What does this function do?
- find "$REPORT_DIR" -name "*.log" -mtime +1 searches for .log files that were last modified more than 1 day ago.
- if [ -n "$old_reports" ] checks if any old files were found (-n tests that the string is not empty).
- tar -czvf compresses those files into a dated archive.
- rm -f $old_reports removes the originals after archiving to free up space.
Next, you need to call this function from the main script so it runs every time the monitor executes.
- Scroll down to the bottom of the file. Find this line:
echo "Report saved to: $REPORT_FILE"
- Add the following two lines directly above echo "Report saved to: $REPORT_FILE":
# Archive old reports
archive_reports
- Save the file by pressing Ctrl+O then Enter.
- Exit nano by pressing Ctrl+X.
- Run the script to confirm everything still works:
./health-monitor.sh
You should see "Report saved to:" followed by the file path. If any of your sample reports were more than a day old, you will also see "Archived old reports." printed.
Seeing a syntax error?
Check that the archive_reports() function has a matching closing brace }. Also make sure the function definition is placed after check_logs() but before the # ----- Main Report ----- comment.
help me fix the syntax error in my health-monitor.sh script
✔️ Awesome, I've got everything!
Great. Double check you have saved health-monitor.sh before moving on.
ⓧ I'd like to double check the full code
Here is the full health-monitor.sh at this point:
#!/bin/bash
# =============================================================
# System Health Monitor
# Generates a formatted health report with warnings
# =============================================================
# Configuration
REPORT_DIR="$HOME/health-monitor/reports"
DISK_THRESHOLD=80
MEM_THRESHOLD=80
REPORT_FILE="$REPORT_DIR/health-$(date +%F-%H%M).log"
# Ensure reports directory exists
mkdir -p "$REPORT_DIR"
# ----- Functions -----
check_disk() {
echo "==== Disk Usage ===="
df -h | grep -v "tmpfs" | grep -v "Filesystem"
echo ""
echo "-- Disk Warnings --"
local warnings=0
while read -r line; do
usage=$(echo "$line" | awk '{print $5}' | sed 's/%//')
mount=$(echo "$line" | awk '{print $6}')
if [ "$usage" -gt "$DISK_THRESHOLD" ] 2>/dev/null; then
echo "[WARNING] $mount is at ${usage}% (threshold: ${DISK_THRESHOLD}%)"
warnings=$((warnings + 1))
fi
done < <(df -h | grep -v "tmpfs" | grep -v "Filesystem")
if [ "$warnings" -eq 0 ]; then
echo "All filesystems within normal limits."
fi
echo ""
}
check_memory() {
echo "==== Memory Usage ===="
free -h
echo ""
local mem_total
local mem_used
local mem_percent
mem_total=$(free -m | awk '/Mem:/ {print $2}')
mem_used=$(free -m | awk '/Mem:/ {print $3}')
mem_percent=$((mem_used * 100 / mem_total))
echo "Memory utilization: ${mem_percent}%"
if [ "$mem_percent" -gt "$MEM_THRESHOLD" ]; then
echo "[WARNING] Memory usage exceeds threshold (${MEM_THRESHOLD}%)"
else
echo "Memory within normal limits."
fi
echo ""
}
check_processes() {
echo "==== Top 5 Processes by Memory ===="
ps aux --sort=-%mem | head -6
echo ""
echo "==== Top 5 Processes by CPU ===="
ps aux --sort=-%cpu | head -6
echo ""
}
check_logs() {
echo "==== Recent Errors/Warnings (dmesg) ===="
local log_output
log_output=$(dmesg 2>/dev/null | grep -i "error\|warning" | tail -5)
if [ -z "$log_output" ]; then
echo "No recent errors or warnings found."
else
echo "$log_output"
fi
echo ""
}
archive_reports() {
local old_reports
old_reports=$(find "$REPORT_DIR" -name "*.log" -mtime +1 2>/dev/null)
if [ -n "$old_reports" ]; then
tar -czvf "$REPORT_DIR/archive-$(date +%F).tar.gz" $old_reports 2>/dev/null
rm -f $old_reports
echo "Archived old reports."
fi
}
# ----- Main Report -----
{
echo "============================================"
echo " System Health Report"
echo " Host: $(hostname)"
echo " Date: $(date)"
echo " Uptime: $(uptime -p)"
echo "============================================"
echo ""
check_disk
check_memory
check_processes
check_logs
echo "==== System Load ===="
uptime | awk -F'load average:' '{print "Load Average:" $2}'
echo ""
echo "============================================"
echo " End of Report"
echo "============================================"
} > "$REPORT_FILE"
# Archive old reports
archive_reports
echo "Report saved to: $REPORT_FILE"
Your script now handles its own housekeeping. Every time it runs, it archives reports older than a day and cleans up the originals. Next up, you will schedule this script to run automatically with cron.
Schedule with Cron
Your health monitor script works perfectly when you run it manually. But real system administrators don't sit around running scripts by hand every few minutes.
Cron is Linux's built-in task scheduler. It runs commands automatically on a schedule you define. In this step, you'll set up a cron job that executes your health monitor every 5 minutes, so reports keep generating even when you're not watching.
In this step, get ready to:
- Start the cron service and verify it is running.
- Add a scheduled job to your crontab that runs the health monitor every 5 minutes.
- Confirm the automation is working by checking for a new report file.
Start the cron service
In WSL2, the cron service doesn't start automatically when you open your terminal. You need to start it manually before any scheduled jobs can run.
- Start the cron service by running this command:
sudo service cron start
What does this command do?
The sudo prefix runs the command with administrator privileges. Starting system services requires elevated permissions.
The service cron start part tells Linux's service manager to launch the cron daemon in the background.
- When prompted, enter your WSL password and press Enter.
- Verify cron is running by checking its status:
service cron status
- You should see output containing the word running or is running.
Why start cron manually in WSL?
In a regular Linux server, cron starts automatically at boot. WSL2 behaves differently. The cron service stops whenever your WSL session shuts down, and it won't restart until you start it again.
For this project, starting it manually is fine. You just need it running long enough to see your scheduled job work.
Seeing "cron: unrecognized service"?
This is rare on Ubuntu but can happen if cron was removed. Install it by running sudo apt install cron and then try starting it again.
If the status shows something other than running, try stopping and restarting with sudo service cron restart.
help me troubleshoot why cron won't start in my WSL2 Ubuntu terminal
Add a cron job to run the health monitor
A crontab is a file where you define your scheduled jobs. Each line specifies when to run a command and what command to run.
- First, check your username so you know the correct path to your script:
whoami
- Note the username that appears. You'll need it in the next command.
- Open the crontab editor by running:
crontab -e
What happens when you run crontab -e?
If this is your first time running crontab -e, you'll see a prompt asking you to select an editor. Choose option 1 for nano, the editor you've been using throughout this project.
- If prompted to select an editor, type 1 and press Enter to choose nano.
- Scroll to the bottom of the file that opens. You'll see comment lines starting with # that explain the crontab format.
- Add the following line at the very bottom of the file, replacing your-username with the username you saw from whoami:
*/5 * * * * /home/[[USERNAME="your-ubuntu-username"]]/health-monitor/health-monitor.sh
What does the cron schedule mean?
The five fields before the command define the schedule. They represent: minute, hour, day of month, month, and day of week.
- */5 in the minute field means "every 5 minutes" (the */ syntax means "every Nth value").
- The four * symbols mean "any value" for hour, day, month, and weekday.
- Combined, this runs the command every 5 minutes, every hour, every day.
- Save the file by pressing Ctrl+O, then press Enter to confirm.
- Exit nano by pressing Ctrl+X.
You should see a message saying crontab: installing new crontab. This confirms your schedule is active.
Don't see the "installing new crontab" message?
Make sure you saved the file before exiting. If you pressed Ctrl+X without saving, nano may have discarded your changes.
Run crontab -e again and check whether your line is at the bottom of the file. If it's missing, add it again and save with Ctrl+O.
help me fix my crontab entry that isn't saving properly
Verify the cron job is working
Now you need to confirm two things: that cron registered your job, and that it actually runs on schedule.
- List your registered cron jobs by running:
crontab -l
- You should see your */5 * * * * line in the output. This confirms cron knows about your job.
- Now wait 5 minutes for the cron job to fire. You can check the current time with:
date
- After 5 minutes have passed, check whether a new report appeared in your reports directory:
ls ~/health-monitor/reports/
- You should see a new .log file with a timestamp matching the time cron ran. This proves your automation is working.
Check which report is newest
If you have many report files, run ls -lt ~/health-monitor/reports/ | head -5 to sort by modification time and show only the 5 most recent.
No new report after 5 minutes?
Double-check that the cron service is still running with service cron status. If it says stopped, start it again with sudo service cron start.
Verify the path in your crontab matches your actual script location. Run which whoami is not needed here. Instead, run ls /home/$(whoami)/health-monitor/health-monitor.sh to confirm the script exists at that exact path.
Also check that the script is executable. Run ls -la ~/health-monitor/health-monitor.sh and confirm you see x in the permissions.
help me figure out why my cron job isn't generating health reports
Secret mission
Network Connectivity Checks
Your health monitor checks disk, memory, and processes. But what about network connectivity? Add a check_network() function that pings a list of servers and flags any that are unreachable, with color-coded output.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally inside WSL2, so there are no ongoing costs.
Resources you used:
- Project directory (~/health-monitor/) with script and report files.
- Cron job scheduled to run health-monitor.sh every 5 minutes.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep experimenting with your health monitor script.
- Your cron job will continue running every 5 minutes as long as WSL is active and the cron service is started.
- New report files will accumulate in ~/health-monitor/reports/.
- The cron job stops automatically when you close WSL or shut down your computer.
Pause - I'll come back to this later
Stop the cron job from generating new reports, but keep all your files in place so you can resume later.
- Open your crontab for editing by running:
crontab -e
- Delete the line that starts with */5 * * * * and save the file.
- Verify the job is removed by running:
crontab -l
You should see no crontab entries. Your script and reports remain on disk for when you return.
Delete - I don't want to use this again
Remove all project resources and the scheduled cron job completely.
- Remove the cron job by opening the crontab editor:
crontab -e
- Delete the line that starts with */5 * * * * and save the file.
- Remove the entire project directory and all report files by running:
rm -rf ~/health-monitor
- Verify the directory is gone by running:
ls ~/health-monitor
You should see a message saying the directory does not exist. Everything is cleaned up.
Nice Work!
Nice Work!
You just built a real system administration tool from scratch using nothing but the Linux terminal. From navigating directories to automating scripts with cron, you now have hands-on experience with the core commands every Linux professional relies on daily.
You've learned how to:
- Write a multi-function bash script that checks disk usage, memory, processes, and logs to generate a formatted system health report.
- Process and filter command output using grep, awk, and pipes to extract exactly the data you need from system commands.
- Automate your script with cron so it runs on a schedule, saves timestamped reports, and archives old ones with tar.
- Added network connectivity checks with ping and colored terminal output to flag unreachable servers.
Ready to quiz yourself?