System Health Monitor with Bash
Write a Bash script that reads Linux kernel data and generates a health report.
Introduction
30 Second Summary
Every server running in the cloud needs something watching its vital signs. CPU load, memory usage, and disk space are the metrics cloud engineers check every single day.
In this project, you will write a Bash script that reads live data from the Linux kernel and generates a color-coded health report. By the end, you will have a reusable monitoring tool that works on any Linux system, including AWS EC2 instances.
What You'll Build
When you run your finished script, you see your system's vital signs at a glance with green for healthy and red for anything that needs attention.
By the end of this project, you'll have:
- A single command (./health-check.sh) that instantly displays CPU load averages, memory usage percentages, and disk space status in a formatted terminal report.
- Color-coded threshold alerts that automatically flag any metric above a configurable limit, so you spot problems at a glance.
- A persistent log file with timestamps that records every health check, letting you review trends across multiple runs.
- An optional Secret Mission to push your skills further.
Are there any prerequisites?
This project requires a Windows PC with WSL (Windows Subsystem for Linux) and the Ubuntu distribution already installed. If you haven't set up WSL yet, follow Microsoft's official WSL installation guide before starting.
Before We Start
Before you dive in, take a moment to lock in what you are about to build and why it matters.
Set Up Your Linux Environment
Every line of your health monitoring script will run inside a Linux terminal. Before you can write a single command, you need to confirm that your Linux environment is set up correctly.
A misconfigured WSL installation or outdated system packages can cause mysterious errors later. This step locks in a clean, verified workspace so everything runs smoothly from here on.
In this step, get ready to:
- Verify your WSL 2 setup and launch Ubuntu.
- Update your system packages and create your project directory.
- Confirm your Bash version is 5.x.
Verify your WSL setup and launch Ubuntu
WSL (Windows Subsystem for Linux) lets you run a full Linux environment inside Windows. This project needs WSL version 2 because it runs a real Linux kernel with full system call compatibility.
- Press the Windows key to open your search bar.
- Type PowerShell and press Enter to open it.
- Check your WSL installation by running this command:
wsl -l -v
This lists every Linux distribution installed on your machine, its current state, and which WSL version it uses.
✔️ I see Ubuntu with VERSION 2
Your output looks something like this:
NAME STATE VERSION
* Ubuntu Running 2
The VERSION column shows 2. You are all set to continue.
ⓧ I see Ubuntu but VERSION is 1
Your Ubuntu distribution is running on WSL 1. Convert it to WSL 2 by running this command in the same PowerShell window:
wsl --set-version Ubuntu 2
This conversion can take a few minutes. Once it finishes, run wsl -l -v again to confirm the VERSION column now shows 2.
ⓧ I don't see Ubuntu listed
Ubuntu is not installed in WSL yet. Install it by running this command in PowerShell:
wsl --install -d Ubuntu
Follow the prompts to create a username and password. Once the installation finishes, run wsl -l -v to confirm Ubuntu appears with VERSION 2.
- Launch your Ubuntu terminal by running this command in the same PowerShell window:
wsl
Your prompt changes to something like username@machine:~$. You are now inside a Linux environment running on your Windows machine.
Don't see the Linux prompt?
If the wsl command hangs or shows an error, try opening Ubuntu directly from your Windows Start menu instead.
If Ubuntu asks you to create a UNIX username and password, go ahead and set those up. You will need the password later for sudo commands.
help me launch Ubuntu on WSL
Update system packages and create the project directory
Outdated packages can cause tools to behave unexpectedly. Updating now ensures every command you run later works with the latest fixes.
- Update your package lists and upgrade installed packages by running this command:
sudo apt update && sudo apt upgrade -y
What does this command do?
- sudo apt update refreshes the list of available packages and their versions from Ubuntu's servers.
- sudo apt upgrade -y installs any newer versions of packages you already have. The -y flag automatically confirms the upgrade so you do not have to type "yes" manually.
- sudo runs the command as an administrator. You will be asked for the password you set when you first created your Ubuntu user.
Prompted about restarting services?
On Ubuntu 22.04 and newer, you may see a dialog asking which services to restart. Press Enter to accept the defaults and continue.
You should see lines scrolling by as packages are fetched and installed. The command finishes when your terminal prompt returns.
- Create the project directory and its logs subdirectory by running this command:
mkdir -p ~/health-monitor/logs
What does this command do?
- mkdir creates a new directory.
- The -p flag creates any missing parent directories along the way. It also does not error if the directory already exists.
- ~/health-monitor/logs creates both health-monitor and the logs folder inside it, in one go.
- Confirm the directory was created by running:
ls ~/health-monitor
You should see logs listed in the output. That is the subdirectory you just created inside health-monitor.
Don't see the logs folder?
Make sure you typed the command with the -p flag. Without it, mkdir fails if the parent health-monitor directory does not exist yet.
help me create the health-monitor directory on Ubuntu
Verify your Bash version
Your health monitoring script uses Bash features that require version 5.x. Before you start writing the script, confirm you have the right version.
Before you run the next command, what version of Bash do you expect to see on a modern Ubuntu installation?
- Check your Bash version by running this command:
bash --version
✔️ I see version 5.x
The first line of output starts with something like GNU bash, version 5.1.16 or a similar 5.x number. You are good to go.
ⓧ I see an older version
A version below 5.0 means your Ubuntu installation is quite old. Upgrade your Ubuntu packages by running:
sudo apt update && sudo apt install --only-upgrade bash -y
Run bash --version again to confirm the version now starts with 5. If it still shows an older version, you may need to upgrade your entire Ubuntu distribution.
ⓧ Command not found
This is very rare on Ubuntu. Install Bash by running:
sudo apt update && sudo apt install bash -y
Run bash --version again to confirm it now shows version 5.x.
- Move into your project directory by running this command:
cd ~/health-monitor && pwd
You should see a path ending in /health-monitor printed to your terminal. This confirms you are in the right place to start building your script.
Seeing "No such file or directory"?
Double check that the mkdir -p ~/health-monitor/logs command from the previous substep ran successfully. Try running it again, then retry the cd command.
help me navigate to my health-monitor directory
Your Linux environment is verified and your project workspace is ready. Next up, you will explore how Bash talks to the Linux kernel to read live system data.
Explore How Bash Reads System Data
Your Linux environment is set up and ready. Now it's time to see the raw data that powers every monitoring tool.
Every monitoring script works by asking the Linux kernel for data. Before you write a single line of script, you need to see this data flow yourself so the script is not a black box.
In this step, get ready to:
- Read live data directly from the Linux kernel through the /proc filesystem.
- Run system commands that format kernel data into readable output.
- Extract specific values using pipes and awk.
Read kernel data directly
The Linux kernel exposes live system information through special files in the /proc directory. The kernel generates this data fresh every time you read these files.
- See your CPU load averages by running this command:
cat /proc/loadavg
You see five space-separated values. The first three numbers are your 1-minute, 5-minute, and 15-minute CPU load averages.
- See raw memory statistics from the kernel by running:
head -5 /proc/meminfo
You see labeled rows like MemTotal and MemFree showing memory values in kilobytes. The kernel updates these numbers continuously.
What is the /proc filesystem?
The /proc directory is a virtual filesystem. It does not store data on your hard drive.
Instead, the kernel creates the contents on the fly when a program reads them. Bash is the translator that fetches this data and displays it in your terminal.
Use system commands that format kernel data
Reading /proc files gives you raw numbers. System commands like free and df read the same kernel data and format it into tables that are easier to understand.
- See your memory usage in a readable table by running:
free -m
You see a table showing total, used, free, and available memory in megabytes. This is the same data as /proc/meminfo, formatted for humans.
- See your disk usage on the root filesystem by running:
df -h /
You see the root filesystem's total size, used space, available space, and usage percentage.
- See how long your system has been running by running:
uptime
You see the current time, uptime duration, and the same load averages from /proc/loadavg.
How does Bash fit in?
Bash is the middleman between you and the Linux kernel. When you type a command, Bash runs it, waits for the kernel to respond, and prints the result in your terminal.
Each command you just ran reads from the kernel and formats the output for humans. Bash handles the back-and-forth so you never have to talk to the kernel directly.
Extract specific values with pipes and awk
Monitoring scripts do not need entire tables. They need single values like one number for used memory or one percentage for disk usage.
- Pull just the 1-minute load average by running:
awk '{print $1}' /proc/loadavg
You see a single number instead of the full line. The awk command extracted just the first field.
- Extract your total memory in MB by running:
free -m | awk '/Mem:/ {print $2}'
You see a single number showing your total memory. The pipe (|) sent the output of free -m into awk, which found the Mem: line and printed the second field.
- Extract your disk usage percentage by running:
df -h / | awk 'NR==2 {print $5}'
You see your disk usage as a percentage like 5%. The NR==2 tells awk to skip the header row and read only the data row.
What are pipes?
The pipe symbol (|) is how Bash chains commands together. It takes the output of the command on the left and feeds it as input to the command on the right.
This is the foundation of every monitoring script. Instead of reading entire tables, you pipe them through awk to extract exactly the value you need.
Look at the free -m table you ran earlier. If you extract field $3 from the Mem: line, what value do you expect to see?
- Test your prediction by running:
free -m | awk '/Mem:/ {print $3}'
You see a single number showing your used memory in MB. Field $3 is the third column of the Mem: line, which is the used value.
Seeing empty output or an error?
Make sure you typed the pipe symbol (|) between the two commands. Without it, Bash treats them as separate commands.
Check that the quotes around the awk pattern are single quotes, not double quotes or backticks.
Help me troubleshoot my awk pipe command.
You have seen the raw kernel data and know how to extract specific values from it. Next up, you will bundle these commands into a reusable script that generates a full system health report.
Build Your Health Report Script
In the previous step, you ran commands like cat /proc/loadavg and free -m one at a time to pull live data from the Linux kernel.
Running commands individually works for quick checks, but it is tedious and unrepeatable. A Bash script bundles those same commands into a reusable program you can run anytime with a single command. The same language you have been typing interactively becomes a program when saved to a file.
In this step, get ready to:
- Create health-check.sh with a shebang and a report header.
- Add CPU load, memory, and disk usage metric sections.
- Add system info and verify the full report runs.
Create the script with the header section
Every Bash script starts with a special first line called a shebang. It tells Linux which program to use when running the file.
- Open a new file in the nano text editor by running this command:
nano ~/health-monitor/health-check.sh
What is nano?
nano is a simple text editor that runs inside your terminal. When you SSH into a remote server like an AWS EC2 instance, there is no VS Code or Notepad. Terminal editors like nano are how you create and edit files.
You should see a blank editing screen with menu options at the bottom.
- Type the following code into the editor to create the shebang, comment header, and report banner:
#!/bin/bash
# System Health Monitor
# Checks CPU load, memory, and disk usage with threshold alerts
# ---- Header ----
echo "======================================"
echo " System Health Report"
echo " Generated: $(date)"
echo " Hostname: $(hostname)"
echo "======================================"
echo ""
What does this code do?
- #!/bin/bash is the shebang. It tells Linux to run this file using Bash, not some other shell.
- $(date) and $(hostname) are command substitutions. Bash runs the command inside $() and inserts its output into the echo string.
- The echo "" at the end prints a blank line to give the report visual spacing.
- Save the file by pressing Ctrl+O, then Enter to confirm the filename.
- Exit nano by pressing Ctrl+X.
- Make the script executable and run it by running these commands:
chmod +x ~/health-monitor/health-check.sh
./health-check.sh
What does chmod +x do?
Linux files have permissions that control who can read, write, and execute them. By default, a new file is not executable. chmod +x adds execute permission so Linux treats the file as a runnable program instead of plain text.
You should see a banner with the report title, the current date and time, and your hostname.
Seeing "Permission denied" or "command not found"?
- If you see Permission denied, the chmod +x step did not run. Run it again and make sure the file path is correct.
- If you see command not found, make sure you are in the ~/health-monitor directory. Run cd ~/health-monitor and try again.
- Check that the first line of your script is exactly #!/bin/bash with no spaces before the #.
Help me debug why my health-check.sh script won't run.
Add the CPU load and memory sections
The banner works. Now you will add the metric sections that read live data from the kernel, starting with CPU load and memory usage.
- Reopen the script in nano by running this command:
nano ~/health-monitor/health-check.sh
- Use the arrow keys to move your cursor to the very end of the file, after the last echo "" line.
- Add the CPU load section by typing the following code below the header:
# ---- CPU Load ----
echo "--- CPU Load ---"
load_1min=$(awk '{print $1}' /proc/loadavg)
load_5min=$(awk '{print $2}' /proc/loadavg)
load_15min=$(awk '{print $3}' /proc/loadavg)
echo " 1 min: $load_1min"
echo " 5 min: $load_5min"
echo " 15 min: $load_15min"
echo ""
What does this code do?
- load_1min=$(awk '{print $1}' /proc/loadavg) captures the 1-minute load average into a variable. This is the same awk command you ran interactively in Step 2, but now stored in a variable for reuse.
- The $(...) syntax is command substitution. Bash runs the command inside and assigns the output to the variable.
- echo " 1 min: $load_1min" prints the value with a label. The $ prefix tells Bash to replace the variable name with its stored value.
- Directly below the CPU load section, add the memory usage section:
# ---- Memory Usage ----
echo "--- Memory Usage ---"
mem_total=$(free -m | awk '/Mem:/ {print $2}')
mem_used=$(free -m | awk '/Mem:/ {print $3}')
mem_available=$(free -m | awk '/Mem:/ {print $7}')
mem_percent=$(awk -v used="$mem_used" -v total="$mem_total" 'BEGIN { printf "%d", (used / total) * 100 }')
echo " Total: ${mem_total} MB"
echo " Used: ${mem_used} MB"
echo " Available: ${mem_available} MB"
echo " Usage: ${mem_percent}%"
echo ""
What does this code do?
- free -m | awk '/Mem:/ {print $2}' pipes the output of free -m into awk, which finds the Mem: row and extracts the total memory value. Fields $3 and $7 grab used and available memory.
- mem_percent uses awk with -v flags to pass the Bash variables into an awk calculation. Bash cannot do float math natively, so awk handles the percentage division.
- Save the file (Ctrl+O, Enter) and exit nano (Ctrl+X).
- Run the script to see your new sections by running this command:
./health-check.sh
You should see the banner from before, followed by a CPU Load section with three load average numbers and a Memory Usage section showing total, used, available, and a percentage.
Seeing a syntax error or missing values?
- If you see a syntax error, check that every $(...) has a matching closing parenthesis. A missing ) is the most common cause.
- If memory values are blank, make sure you used single quotes around the awk pattern (e.g. '/Mem:/ {print $2}'). Double quotes would cause Bash to interpret the $2 before awk sees it.
Help me debug a syntax error in my health-check.sh script.
Add disk usage, system info, and the closing banner
The script covers CPU and memory. The final piece is disk usage, system uptime, and a closing banner to frame the report.
- Reopen the script in nano by running this command:
nano ~/health-monitor/health-check.sh
- Use the arrow keys to move your cursor to the end of the file, after the last echo "" line in the memory section.
- Add the disk usage section, system info section, and closing banner by typing the following code:
# ---- Disk Usage ----
echo "--- Disk Usage ---"
disk_total=$(df -h / | awk 'NR==2 {print $2}')
disk_used=$(df -h / | awk 'NR==2 {print $3}')
disk_avail=$(df -h / | awk 'NR==2 {print $4}')
disk_usage=$(df -h / | awk 'NR==2 {print $5}')
disk_percent=${disk_usage%\%}
echo " Total: $disk_total"
echo " Used: $disk_used"
echo " Available: $disk_avail"
echo " Usage: $disk_usage"
echo ""
# ---- System Info ----
echo "--- System Info ---"
echo " Uptime: $(uptime -p)"
echo " Kernel: $(uname -r)"
echo ""
echo "======================================"
echo " Health check complete"
echo "======================================"
What does this code do?
- df -h / | awk 'NR==2 {print $2}' gets the size of the root filesystem. NR==2 selects the second row (skipping the header), and $2 through $5 extract total, used, available, and usage percentage.
- ${disk_usage%\%} strips the trailing % character from the disk usage string. The ${variable%pattern} syntax is Bash parameter expansion that removes a suffix. This gives you a plain number you can compare against thresholds later.
- $(uptime -p) prints how long the system has been running in a human-readable format. $(uname -r) prints the Linux kernel version.
- Save the file (Ctrl+O, Enter) and exit nano (Ctrl+X).
Before you run the script, what sections do you expect to see in the output?
- Run the completed script by running this command:
./health-check.sh
You should see a full formatted report with a header banner, CPU load averages, memory usage with a percentage, disk usage statistics, system uptime, kernel version, and a closing banner.
Disk values showing blank or unexpected output?
- If disk values are blank, check that you have a space between df -h / and the pipe |. A missing space around the pipe will cause a parsing error.
- If the disk_percent line causes an error, check that you typed ${disk_usage%\%} exactly, including the backslash before the second %.
Help me troubleshoot my disk usage section in health-check.sh.
Double check your script matches the complete file below.
✔️ Awesome, I've got everything!
Great. Make sure you have saved health-check.sh and that the report printed all five sections (header, CPU, memory, disk, system info).
ⓧ I'd like to double check the full code
Open health-check.sh in nano and scroll through the file. Confirm each of these sections appears in order:
- The shebang (#!/bin/bash) and comment lines at the top.
- The Header section with echo lines for the report title, date, and hostname.
- The CPU Load section with load_1min, load_5min, and load_15min variables reading from /proc/loadavg.
- The Memory Usage section with mem_total, mem_used, mem_available, and mem_percent variables.
- The Disk Usage section with disk_total, disk_used, disk_avail, disk_usage, and disk_percent variables.
- The System Info section with uptime -p and uname -r.
- The closing banner with "Health check complete".
Your script generates a complete system health report with a single command. Right now the output is all raw numbers. In the next step, you will add color-coded alerts so you can instantly spot which metrics need attention.
Add Threshold Alerts
Your health report now prints every system vital. But right now, every metric is just a raw number. When you are managing multiple servers, you need to instantly see what is healthy and what needs attention.
In this step, you will add configurable thresholds to the script. Each metric gets compared against a limit, and the result is displayed as a color-coded [OK] or [WARNING] status. No more mentally comparing every number yourself.
In this step, get ready to:
- Define threshold variables and ANSI color codes.
- Write conditional checks that compare each metric against its threshold.
- See color-coded status indicators in the terminal output.
Add configuration and the CPU threshold check
Before writing any conditional logic, the script needs to know two things: what the threshold limits are, and how to print in color. You will add both of these as variables at the top of the file.
- Open health-check.sh in nano by running this command:
nano ~/health-monitor/health-check.sh
- Find the line that says # Checks CPU load, memory, and disk usage with threshold alerts.
- Position your cursor at the end of that line and press Enter to create a blank line.
- Add the following configuration and color code block before the # ---- Header ---- line:
# ---- Configuration ----
CPU_THRESHOLD=2
MEMORY_THRESHOLD=80
DISK_THRESHOLD=80
# ---- Colors ----
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
What do these variables do?
- CPU_THRESHOLD=2 sets the load average limit. A load average above the number of CPU cores means the system is under contention. 2 is a reasonable default for a WSL system.
- MEMORY_THRESHOLD=80 and DISK_THRESHOLD=80 set the percentage limits for memory and disk usage.
- RED, GREEN, and NC are ANSI escape codes that tell the terminal to display text in color. NC stands for "no color" and resets the terminal back to its default.
Now add the first conditional check so the CPU load section shows a status indicator.
- Scroll down to the # ---- CPU Load ---- section.
- Find the line echo " 15 min: $load_15min".
- Add the following code between that line and the echo "" below it:
cpu_check=$(awk -v load="$load_1min" -v thresh="$CPU_THRESHOLD" 'BEGIN { if (load >= thresh) print "WARN"; else print "OK" }')
if [ "$cpu_check" = "WARN" ]; then
echo -e " Status: ${RED}[WARNING] Load exceeds threshold of ${CPU_THRESHOLD}${NC}"
else
echo -e " Status: ${GREEN}[OK]${NC}"
fi
Why use awk instead of a regular if statement?
Load averages are decimal numbers like 0.12. Bash can only compare whole integers natively. The awk command handles float arithmetic, so it compares the load average against the threshold and prints either WARN or OK.
The echo -e command interprets the ANSI escape codes stored in $RED and $GREEN, turning the status text red or green in the terminal.
- Save the file by pressing Ctrl+O, then Enter, then Ctrl+X to exit nano.
- Run the script to see the CPU status by running:
./health-check.sh
Under the CPU Load section, you should now see a green [OK] status line. The memory and disk sections still show raw numbers with no status.
No color showing up?
- Make sure you typed echo -e (with the -e flag), not just echo. The -e flag tells Bash to interpret the color escape codes.
- Check that the color variables use single quotes, not double quotes: RED='\033[0;31m'.
- help me debug why colors are not appearing in my Bash script output
Add memory and disk threshold checks
Memory and disk percentages are whole numbers, so Bash can compare them directly with its built-in integer comparison. No awk needed this time.
- Open the script again in nano by running:
nano ~/health-monitor/health-check.sh
- Scroll down to the # ---- Memory Usage ---- section.
- Find the line echo " Usage: ${mem_percent}%".
- Add the following code between that line and the echo "" below it:
if [ "$mem_percent" -ge "$MEMORY_THRESHOLD" ]; then
echo -e " Status: ${RED}[WARNING] Memory usage exceeds ${MEMORY_THRESHOLD}%${NC}"
else
echo -e " Status: ${GREEN}[OK]${NC}"
fi
What does -ge mean?
The -ge operator means "greater than or equal to" in Bash integer comparison. Bash uses these letter-based operators (-ge, -lt, -eq) instead of symbols like >= for comparing numbers inside [ ] brackets.
- Now scroll down to the # ---- Disk Usage ---- section.
- Find the line echo " Usage: $disk_usage".
- Add the following code between that line and the echo "" below it:
if [ "$disk_percent" -ge "$DISK_THRESHOLD" ]; then
echo -e " Status: ${RED}[WARNING] Disk usage exceeds ${DISK_THRESHOLD}%${NC}"
else
echo -e " Status: ${GREEN}[OK]${NC}"
fi
- Save and exit nano (Ctrl+O, Enter, Ctrl+X).
- Run the script again by running:
./health-check.sh
Every metric section now ends with a green [OK] status line. CPU, memory, and disk all have their own threshold checks.
Seeing a syntax error or missing status lines?
- Make sure every if block ends with fi. A missing fi causes Bash to throw a syntax error.
- Check that the square brackets have spaces inside them: [ "$mem_percent" -ge "$MEMORY_THRESHOLD" ]. Bash requires spaces around the brackets and between operands.
- Verify that the new code blocks are placed between each metric's last echo line and the echo "" below it, not after echo "".
- help me fix syntax errors in my Bash if/else threshold checks
✔️ Awesome, I've got everything!
Great. Make sure you have saved health-check.sh and that all three metric sections show a status line.
ⓧ I'd like to double check the full code
The full file is longer than a quick glance, so scroll up through the chunks you just added and confirm each one is present, in order:
- The # ---- Configuration ---- block with three threshold variables, followed by the # ---- Colors ---- block with RED, GREEN, and NC. These sit between the comment header and the # ---- Header ---- section.
- The cpu_check awk line and its if/else/fi block, placed after the CPU load echo lines.
- The memory if [ "$mem_percent" -ge "$MEMORY_THRESHOLD" ] block, placed after the memory echo lines.
- The disk if [ "$disk_percent" -ge "$DISK_THRESHOLD" ] block, placed after the disk echo lines.
If anything is missing or misplaced, open nano ~/health-monitor/health-check.sh and fix it. Save, then re-run ./health-check.sh.
Test the warning alerts
On a lightly used WSL system, all three metrics will likely show green [OK]. To prove the warnings actually work, you can temporarily lower a threshold so it triggers.
- Open the script in nano by running:
nano ~/health-monitor/health-check.sh
- Find the line near the top that says:
MEMORY_THRESHOLD=80
- Change 80 to 1 so the line now reads:
MEMORY_THRESHOLD=1
- Save and exit nano (Ctrl+O, Enter, Ctrl+X).
Before you run the script, what do you think the memory status will show now that the threshold is set to 1%?
- Run the script to find out:
./health-check.sh
The memory section now shows a red [WARNING] status. Since nearly any amount of memory usage exceeds 1%, the alert fires. CPU and disk should still show green [OK].
- Now change MEMORY_THRESHOLD back to 80. Open the script in nano by running:
nano ~/health-monitor/health-check.sh
- Find the line MEMORY_THRESHOLD=1 and change 1 back to 80 so it reads MEMORY_THRESHOLD=80.
- Save and exit nano (Ctrl+O, Enter, Ctrl+X).
Your script now makes it instantly obvious which metrics are healthy and which need attention. Next up, you will add logging so these results are saved to a file and persist after you close the terminal.
Add Logging
Your script now flags problems with color-coded alerts. A quick glance tells you what is healthy and what needs attention.
But that report vanishes the moment you close the terminal. On a real server, you need a record of every health check so you can spot trends and prove what happened during an incident. In this step, you will add persistent logging that writes timestamped entries to a file every time the script runs.
In this step, get ready to:
- Add a logging function that writes timestamped entries to a log file.
- Add log calls after each metric check and at the start and end of the script.
- Verify that the log accumulates entries across multiple runs.
Add the log file path and logging function
Your script needs two things to start logging: a file path to write to, and a reusable function that formats each log entry with a timestamp.
- Open health-check.sh in nano by running this command:
nano ~/health-monitor/health-check.sh
- Find the line NC='\033[0m' at the end of the # ---- Colors ---- section.
- Add a blank line below it, then paste the following code before the # ---- Header ---- comment:
# ---- Logging ----
LOG_FILE="$HOME/health-monitor/logs/health-report.log"
mkdir -p "$(dirname "$LOG_FILE")"
log_message() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
What does this code do?
- LOG_FILE stores the path to the log file. Using $HOME instead of a hardcoded path means the script works for any user.
- mkdir -p "$(dirname "$LOG_FILE")" ensures the logs directory exists before the script tries to write to it.
- log_message() is a function that takes a message, wraps it with a timestamp using date, and appends it to the log file. The >> operator appends instead of overwriting, so every run adds to the file.
Now add the first log call so you can test the function right away.
- Scroll down to the # ---- Header ---- section.
- Find the echo "" line that comes right after the closing ====== banner.
- Add this line directly below it:
log_message "Health check started"
- Save the file with Ctrl+O, press Enter to confirm, then exit with Ctrl+X.
- Run the script to test logging by running this command:
./health-check.sh
The terminal output looks the same as before. The interesting part is what happened behind the scenes.
- Check the log file by running this command:
cat ~/health-monitor/logs/health-report.log
You should see a single timestamped entry: [2026-08-21 14:30:45] Health check started (with your current date and time).
Not seeing the log entry?
- Make sure you added the # ---- Logging ---- section between the # ---- Colors ---- section and the # ---- Header ---- section. The function must be defined before it is called.
- Check that >> appears in the function, not a single >. A single > overwrites the file each time instead of appending.
- Why isn't my log file being created in health-check.sh?
Add log calls to each metric check
The logging function works. Now you need to call it after each metric's conditional check so the log records whether each metric passed or triggered a warning.
- Open health-check.sh in nano by running this command:
nano ~/health-monitor/health-check.sh
- Scroll down to the # ---- CPU Load ---- section and find the if [ "$cpu_check" = "WARN" ]; then block.
- Update the entire if/else block to match the following code, adding a log_message call after each echo -e line:
if [ "$cpu_check" = "WARN" ]; then
echo -e " Status: ${RED}[WARNING] Load exceeds threshold of ${CPU_THRESHOLD}${NC}"
log_message "WARNING: CPU load ${load_1min} exceeds threshold ${CPU_THRESHOLD}"
else
echo -e " Status: ${GREEN}[OK]${NC}"
log_message "OK: CPU load ${load_1min}"
fi
What's the pattern here?
Each branch of the if/else already prints a status to the terminal. The new log_message line mirrors that status into the log file. The terminal shows colors for the human sitting at the screen. The log file records the plain text for later review.
- Now scroll down to the # ---- Memory Usage ---- section and find its if [ "$mem_percent" -ge "$MEMORY_THRESHOLD" ]; then block.
- Update it to match this code, following the same pattern:
if [ "$mem_percent" -ge "$MEMORY_THRESHOLD" ]; then
echo -e " Status: ${RED}[WARNING] Memory usage exceeds ${MEMORY_THRESHOLD}%${NC}"
log_message "WARNING: Memory usage ${mem_percent}% exceeds threshold ${MEMORY_THRESHOLD}%"
else
echo -e " Status: ${GREEN}[OK]${NC}"
log_message "OK: Memory usage ${mem_percent}%"
fi
- Save the file with Ctrl+O, press Enter, then exit with Ctrl+X.
- Run the script and check the log to verify CPU and memory entries appear by running these commands:
./health-check.sh && cat ~/health-monitor/logs/health-report.log
The log now shows your earlier "Health check started" entry plus new entries for CPU and memory status from this run.
- Open health-check.sh in nano again by running this command:
nano ~/health-monitor/health-check.sh
- Scroll down to the # ---- Disk Usage ---- section and find its if [ "$disk_percent" -ge "$DISK_THRESHOLD" ]; then block.
- Update it to add log_message calls following the same pattern:
if [ "$disk_percent" -ge "$DISK_THRESHOLD" ]; then
echo -e " Status: ${RED}[WARNING] Disk usage exceeds ${DISK_THRESHOLD}%${NC}"
log_message "WARNING: Disk usage ${disk_percent}% exceeds threshold ${DISK_THRESHOLD}%"
else
echo -e " Status: ${GREEN}[OK]${NC}"
log_message "OK: Disk usage ${disk_percent}%"
fi
Two more changes wrap up the logging. The script needs a closing log entry and a way to tell the user where the log was saved.
- Scroll to the bottom of the file and find the closing banner:
echo "======================================"
echo " Health check complete"
echo "======================================"
- Replace that block with the following updated version, which adds a log path line and a final log_message call:
echo "======================================"
echo " Health check complete"
echo " Log saved to: $LOG_FILE"
echo "======================================"
log_message "Health check completed"
What does this change do?
The new echo line shows the log file path in the terminal so the user always knows where to find the log. The log_message "Health check completed" call bookends the run. Every log entry between "started" and "completed" belongs to one run.
- Save the file with Ctrl+O, press Enter, then exit with Ctrl+X.
✔️ Awesome, I've got everything!
Great. Double check you have saved health-check.sh and that you exited nano.
ⓧ I'd like to double check the full code
The full file is long, so scroll through your script in nano and confirm each section is present, in this order:
- Shebang (#!/bin/bash) and comment at the top.
- Configuration section with CPU_THRESHOLD, MEMORY_THRESHOLD, and DISK_THRESHOLD.
- Colors section with RED, GREEN, and NC.
- Logging section with LOG_FILE, mkdir -p, and the log_message() function.
- Header section, followed by log_message "Health check started".
- CPU, memory, and disk sections, each with a log_message call in both the if and else branches.
- System info section (unchanged).
- Closing banner with Log saved to: $LOG_FILE line, followed by log_message "Health check completed".
Verify the accumulated log
The whole point of logging with >> is that entries accumulate across multiple runs. Time to prove it works.
- Clear the existing log so you start fresh by running this command:
> ~/health-monitor/logs/health-report.log
What does this command do?
The > operator with no input empties the file without deleting it. This gives you a clean slate for the accumulation test.
- Run the health check script twice by running these commands:
./health-check.sh
./health-check.sh
Before you check the log, think about what you expect to see. How many groups of entries should be there? What should the first and last entry of each group say?
- View the accumulated log by running this command:
cat ~/health-monitor/logs/health-report.log
You should see two groups of timestamped entries. Each group starts with "Health check started" and ends with "Health check completed", with OK or WARNING lines for CPU, memory, and disk in between.
Why does this matter on a real server?
On an EC2 instance, you would schedule this script to run every few minutes using cron. The log file becomes a timeline of your server's health. When something goes wrong at 3 AM, you can trace back through the entries to see exactly when a metric crossed its threshold.
Log file is empty or missing entries?
- Open the script in nano and check that the log_message() function uses >> (double angle bracket), not >. A single > overwrites the file on each call, so only the last entry survives.
- Make sure every log_message call uses double quotes around the message string. Single quotes would prevent variable expansion, so ${mem_percent} would appear literally instead of as a number.
- Why is my health-check.sh log file missing entries or empty after running twice?
Your health monitor now has a memory. Every run is recorded with timestamps, and you can review the history anytime. Next up, you will take on an optional Secret Mission to add process monitoring, so you can catch the top resource-hungry culprits on your system.
Secret mission
Monitor Running Processes
Your health report tracks CPU load, memory, and disk usage. But when a metric spikes on a production server, you need to know which process is responsible. Add process monitoring to catch the top resource-hungry culprits.
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 WSL, so there are no ongoing costs.
Resources you used:
- The ~/health-monitor/ directory containing your health-check.sh script and logs/ folder.
Keep everything running
No action needed. Choose this if you plan to keep using your health monitoring script or want to extend it further.
- Your health-check.sh script stays at ~/health-monitor/health-check.sh and is ready to run anytime with ./health-check.sh.
- Your log file at ~/health-monitor/logs/health-report.log continues to accumulate entries each time you run the script.
- The script uses no background processes or services, so nothing is consuming resources while it is not running.
Pause - I'll come back to this later
Since the script only runs when you execute it manually, there is nothing to pause. Your files will stay exactly where they are until you return.
- If your log file has grown large, you can clear it while keeping the file by running this in your WSL terminal:
> ~/health-monitor/logs/health-report.log
- This empties the log file without deleting it, so your script continues to work when you come back.
Delete - I don't want to use this again
Remove all project files from your WSL filesystem. This deletes the script, log file, and the entire project directory.
- Open your WSL Ubuntu terminal and run the following command to delete the project directory:
rm -rf ~/health-monitor
- Confirm the directory has been removed by running:
ls ~/health-monitor
- You should see a message saying the directory cannot be found. This confirms all project files have been deleted.
Nice Work!
Nice Work!
Nice work! You just built a real system health monitoring tool from scratch using Bash on WSL Ubuntu.
You've learned how to:
- Read live data straight from the Linux kernel through /proc/loadavg and /proc/meminfo, then extract specific values using pipes and awk.
- Write a reusable Bash script with variables, conditionals, functions, and output redirection that generates a formatted system health report for CPU load, memory usage, and disk space.
- Add color-coded threshold alerts and persistent timestamped logging so you can spot problems at a glance and review trends over time.
- Secret Mission: Add process monitoring with ps to catch the top resource-hungry processes by CPU and memory usage.
Ready to quiz yourself?