Build a System Health Monitor
Write a Python script that monitors CPU, memory, and disk usage.
Introduction
30 Second Summary
Think about how you check your phone's battery and storage when it starts running slow. Server engineers do the same thing for the computers that run apps and websites, except they use scripts to automate it.
In this project, you will build a Python script that monitors your computer's CPU, memory, and disk usage. It flags anything that looks unhealthy and saves a timestamped log of what it finds.
What You'll Build
A command-line system health monitor that checks your machine's vital signs, flags metrics exceeding configurable thresholds, and logs everything to a file with timestamps.
By the end of this project, you'll have:
- A working Python script that collects live CPU, memory, and disk metrics from your machine.
- A log file with timestamped health data and threshold-based alerts.
- Hands-on experience with psutil, subprocess, and logging modules that DevOps engineers use daily.
- Secret Mission: Add network monitoring and export metrics to a JSON report file.
Are there any prerequisites?
None. This guide includes full setup instructions for Python and all required tools. You don't need any prior programming experience to get started.
Before We Start
Before We Start...
Set Up Your Python Environment
Before you can monitor your system's health, you need the right tools installed. Python is the language DevOps engineers reach for when writing automation scripts, and it comes with powerful standard libraries for interacting with your operating system.
In this step, you'll install Python, create an isolated project environment, and add the one external library your monitor needs. This ensures your project has a clean, repeatable setup that won't conflict with anything else on your machine.
In this step, get ready to:
- Install and verify Python 3.13+ on your machine.
- Create a project folder with a virtual environment.
- Install the psutil library for system monitoring.
Install Python 3.13+
Python is the scripting language you'll use for the entire project. You need version 3.13 or higher to follow along.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Check your Python version by running this command:
python3 --version
Windows users
On Windows, the command may be python --version instead of python3 --version. Try both and use whichever one responds.
✔️ I see version 3.13 or higher
You're all set. Your output looks something like Python 3.13.13 or Python 3.14.5. Move on to the next substep.
ⓧ I see an older version
macOS often ships with an older Python version by default. Even after installing a newer version, your terminal may still point to the old one.
- Download and install Python 3.13 from the official downloads page.
- On macOS, the installer adds python3 to your PATH. After installing, close and reopen your terminal.
- On Windows, make sure you check Add Python to PATH during installation.
- Run python3 --version again (or python --version on Windows) to confirm you see 3.13 or higher.
Still seeing the old version on macOS?
Try running python3.13 --version directly. If that works, use python3.13 in place of python3 for the rest of this project.
ⓧ Command not found
Python isn't installed yet. Let's fix that.
- Go to the official Python downloads page.
- Download the latest Python 3.13 installer for your operating system.
- Run the installer. On Windows, check the Add Python to PATH checkbox before clicking Install Now.
- Close and reopen your terminal after installation.
- Run python3 --version (or python --version on Windows) to confirm you see 3.13 or higher.
Create a project folder and set up a virtual environment
A virtual environment is an isolated space where your project's dependencies live. It prevents conflicts between packages used by different projects on your machine.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir health-monitor
cd health-monitor
- Confirm the folder exists by running:
ls
You should see the health-monitor folder listed on your Desktop. Since you already moved into it, the ls output will be empty (the folder is new and has nothing in it yet).
- Create a virtual environment inside your project folder by running:
python3 -m venv venv
What does this command do?
python3 -m venv venv tells Python to run its built-in venv module and create a virtual environment in a folder called venv.
This gives you an isolated Python installation. Any packages you install next stay inside this folder and won't affect the rest of your system.
- Activate the virtual environment so your terminal uses it:
macOS
source venv/bin/activate
Windows
.\venv\Scripts\Activate.ps1
Seeing a permissions error?
PowerShell may block script execution by default. Run this command once to allow it:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Then try the activate command again. Help me fix PowerShell execution policy errors.
You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
Make sure you ran the activate command from inside the health-monitor folder. Run pwd (macOS/Linux) or cd (Windows) to check your current location.
If you're in the right folder but activation failed, check that the venv folder was created by running ls. You should see a venv directory listed. Help me activate my virtual environment.
Install psutil
psutil is a cross-platform library that gives Python access to system metrics like CPU usage, memory, and disk space. It works on macOS, Windows, and Linux without any code changes.
- Install psutil in your virtual environment by running:
pip install psutil==7.2.2
You should see output ending with Successfully installed psutil-7.2.2.
Install failed or pip not found?
Make sure your virtual environment is active. You should see (venv) in your prompt. If not, go back and run the activate command from the previous substep.
If you see a permissions error, do not use sudo. Instead, confirm the virtual environment is active. Help me fix my pip install error.
- Verify that psutil installed correctly by running:
python -c "import psutil; print(psutil.__version__)"
You should see 7.2.2 printed in your terminal. This confirms the library is installed and importable.
Why pin a specific version?
Using psutil==7.2.2 instead of just psutil locks the exact version. This ensures your project behaves the same way regardless of when you install it. Version pinning is a standard DevOps practice for reproducible builds.
Your environment is ready to go. Next up, you'll write the core function that collects live metrics from your machine's CPU, memory, and disk.
Collect System Metrics with psutil
Your Python environment is set up and psutil is installed. Now it's time to write the script that actually monitors your system.
In this step, you'll use psutil to pull live metrics from your machine's CPU, memory, and disk. You'll also build a display function that formats everything into a clean terminal report.
In this step, get ready to:
- Create health_monitor.py with a function that collects system metrics.
- Build a display function that prints a formatted health report.
- Run the script and see your machine's live stats in the terminal.
Create the health monitor script
The first job is to build a function that asks your operating system for its current CPU, memory, and disk usage. psutil wraps all the low-level OS calls into simple Python methods.
- Create a new file called health_monitor.py in your project folder by running this command:
touch health_monitor.py
- Confirm the file exists by running:
ls
You should see health_monitor.py listed alongside your venv folder.
- Open health_monitor.py in your editor and paste the following code:
#!/usr/bin/env python3
"""
System Health Monitor
Monitors CPU, memory, and disk usage with threshold-based alerting.
Logs all metrics to a file and displays a summary in the terminal.
"""
import psutil
from datetime import datetime
def get_system_metrics():
"""Collect system metrics including CPU, memory, and disk usage."""
# Get CPU usage as a percentage (blocks for 1 second to measure)
cpu_usage = psutil.cpu_percent(interval=1)
# Get memory stats
memory = psutil.virtual_memory()
memory_usage = memory.percent
# Get disk stats for the root partition
disk = psutil.disk_usage("/")
disk_usage = disk.percent
# Return all metrics in a dictionary
return {
"cpu": cpu_usage,
"memory": memory_usage,
"disk": disk_usage,
"memory_total_gb": round(memory.total / (1024**3), 2),
"memory_used_gb": round(memory.used / (1024**3), 2),
"disk_total_gb": round(disk.total / (1024**3), 2),
"disk_used_gb": round(disk.used / (1024**3), 2),
}
What does this code do?
psutil.cpu_percent(interval=1) measures how busy your CPU is over a 1-second window. The interval=1 parameter tells Python to pause for one second while it samples the CPU, giving you an accurate reading.
psutil.virtual_memory() returns a named tuple with fields like total, used, and percent. Similarly, psutil.disk_usage("/") returns total, used, free, and percent for your root disk.
The function packs everything into a Python dictionary so the rest of your script can access any metric by name.
- Save health_monitor.py.
You won't see any output yet. The next substep adds a display function that prints these metrics.
Seeing a red underline on import psutil?
Your editor might not recognize psutil if it's pointing at the wrong Python interpreter. Make sure your virtual environment is active in the terminal (you should see (venv) in your prompt). If your editor has a Python interpreter selector, point it to the venv folder's Python.
help me fix my psutil import error
Add a display function for the health report
Raw numbers in a dictionary aren't easy to read at a glance. A display function formats the metrics into a report you can scan in seconds.
- In health_monitor.py, add the following function below get_system_metrics():
def display_metrics(metrics):
"""Display a formatted health report in the terminal."""
# Print a header with the current time
print("\n" + "=" * 50)
print(" SYSTEM HEALTH REPORT")
print("=" * 50)
print(f" Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("-" * 50)
# Print each metric with its value
print(f" CPU Usage: {metrics['cpu']:.1f}%")
print(
f" Memory Usage: {metrics['memory']:.1f}% "
f"({metrics['memory_used_gb']} / {metrics['memory_total_gb']} GB)"
)
print(
f" Disk Usage: {metrics['disk']:.1f}% "
f"({metrics['disk_used_gb']} / {metrics['disk_total_gb']} GB)"
)
print("=" * 50 + "\n")
What does this code do?
This function takes the metrics dictionary from get_system_metrics() and prints each value in a bordered report. The :.1f format specifier rounds floats to one decimal place.
The datetime.now().strftime('%Y-%m-%d %H:%M:%S') call stamps the report with the exact time you ran it. This becomes important when you add logging later.
- Save health_monitor.py.
You won't see output yet. The next substep adds the entry point that ties both functions together.
Run the script to see your system's health
Python scripts need an entry point that calls your functions when you run the file. The if __name__ == "__main__" pattern ensures your code only runs when executed directly, not when imported by another script.
- At the bottom of health_monitor.py, add the following code:
if __name__ == "__main__":
metrics = get_system_metrics()
display_metrics(metrics)
- Save health_monitor.py.
- Run the script by entering this command in your terminal:
python3 health_monitor.py
After a 1-second pause (while psutil measures your CPU), you should see a formatted report showing your system's current CPU, memory, and disk usage percentages.
What should I see?
Your output will look something like this (your numbers will differ based on your machine):
==================================================
SYSTEM HEALTH REPORT
==================================================
Timestamp: 2026-06-03 14:22:01
--------------------------------------------------
CPU Usage: 12.3%
Memory Usage: 67.4% (10.78 / 16.0 GB)
Disk Usage: 54.2% (245.3 / 452.5 GB)
==================================================
Not seeing any output?
Make sure your virtual environment is still active. You should see (venv) at the start of your terminal prompt. If not, re-activate it with source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows).
Also check that the if __name__ == "__main__": block is at the very bottom of your file, not indented inside another function.
help me fix my health_monitor.py script
✔️ Awesome, I've got everything!
Great. Double check you've saved your file, then move on to the next step.
ⓧ I'd like to double check the full code
Here's the complete health_monitor.py file at this point:
#!/usr/bin/env python3
"""
System Health Monitor
Monitors CPU, memory, and disk usage with threshold-based alerting.
Logs all metrics to a file and displays a summary in the terminal.
"""
import psutil
from datetime import datetime
def get_system_metrics():
"""Collect system metrics including CPU, memory, and disk usage."""
cpu_usage = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
memory_usage = memory.percent
disk = psutil.disk_usage("/")
disk_usage = disk.percent
return {
"cpu": cpu_usage,
"memory": memory_usage,
"disk": disk_usage,
"memory_total_gb": round(memory.total / (1024**3), 2),
"memory_used_gb": round(memory.used / (1024**3), 2),
"disk_total_gb": round(disk.total / (1024**3), 2),
"disk_used_gb": round(disk.used / (1024**3), 2),
}
def display_metrics(metrics):
"""Display a formatted health report in the terminal."""
print("\n" + "=" * 50)
print(" SYSTEM HEALTH REPORT")
print("=" * 50)
print(f" Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("-" * 50)
print(f" CPU Usage: {metrics['cpu']:.1f}%")
print(
f" Memory Usage: {metrics['memory']:.1f}% "
f"({metrics['memory_used_gb']} / {metrics['memory_total_gb']} GB)"
)
print(
f" Disk Usage: {metrics['disk']:.1f}% "
f"({metrics['disk_used_gb']} / {metrics['disk_total_gb']} GB)"
)
print("=" * 50 + "\n")
if __name__ == "__main__":
metrics = get_system_metrics()
display_metrics(metrics)
Your monitor is collecting real data from your machine. Next up, you'll add threshold-based alerting so the script can warn you when a metric gets dangerously high.
Add Threshold-Based Alerting
Your script can now report what's happening on your machine. But a wall of numbers isn't useful unless you know which ones need attention.
In this step, you'll add threshold-based alerting. The script will compare each metric against a safe limit and flag anything that's too high with a clear warning.
In this step, get ready to:
- Define configurable alert thresholds for CPU, memory, and disk.
- Write a function that checks metrics against those thresholds.
- Update the display to show WARNING labels and an alerts summary.
Define alert thresholds
Before you can flag a problem, you need to define what counts as a problem. A thresholds dictionary stores the maximum acceptable percentage for each metric.
- In health_monitor.py, find the line that says from datetime import datetime. Copy the following code below it:
# Configuration
THRESHOLDS = {
"cpu": 80.0,
"memory": 85.0,
"disk": 90.0,
}
What does this code do?
This dictionary maps each metric name to a maximum safe percentage. If CPU goes above 80%, memory above 85%, or disk above 90%, that's a problem worth flagging.
Storing these values in a dictionary at the top of the file makes them easy to adjust later without digging through function logic.
- Save health_monitor.py.
- You won't see a change yet. The next chunk uses these thresholds in a function.
Write the threshold checking function
Now you need a function that loops through each threshold and checks whether the corresponding metric exceeds it. If it does, the function collects a warning message.
- In health_monitor.py, find the get_system_metrics() function. Copy the following new function below it (after the return statement's closing brace):
def check_thresholds(metrics):
"""Check metrics against thresholds and return a list of warnings."""
warnings = []
for metric_name, threshold in THRESHOLDS.items():
value = metrics.get(metric_name, 0)
if value > threshold:
warnings.append(
f"{metric_name.upper()} usage is {value:.1f}% "
f"(threshold: {threshold:.1f}%)"
)
return warnings
What does this code do?
The function loops through every key-value pair in THRESHOLDS. For each one, it looks up the matching metric and compares.
If a metric exceeds its threshold, the function builds a human-readable warning string and appends it to the list. At the end, it returns all the warnings (or an empty list if everything is healthy).
- Save health_monitor.py.
- You won't see a change yet. The next substep connects this function to the display output.
Update the display to show warnings
The display function needs to do two new things: print a [WARNING] label next to any metric that's too high, and show an alerts section at the bottom summarizing what went wrong.
- In health_monitor.py, find the existing display_metrics function. Replace the entire function with this updated version:
def display_metrics(metrics, warnings):
"""Display a formatted health report in the terminal."""
print("\n" + "=" * 50)
print(" SYSTEM HEALTH REPORT")
print("=" * 50)
print(f" Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("-" * 50)
cpu_status = " [WARNING]" if metrics["cpu"] > THRESHOLDS["cpu"] else ""
mem_status = " [WARNING]" if metrics["memory"] > THRESHOLDS["memory"] else ""
disk_status = " [WARNING]" if metrics["disk"] > THRESHOLDS["disk"] else ""
print(f" CPU Usage: {metrics['cpu']:.1f}%{cpu_status}")
print(
f" Memory Usage: {metrics['memory']:.1f}% "
f"({metrics['memory_used_gb']} / {metrics['memory_total_gb']} GB){mem_status}"
)
print(
f" Disk Usage: {metrics['disk']:.1f}% "
f"({metrics['disk_used_gb']} / {metrics['disk_total_gb']} GB){disk_status}"
)
print("-" * 50)
if warnings:
print(" ALERTS:")
for warning in warnings:
print(f" ⚠ {warning}")
else:
print(" Status: All systems healthy ✓")
print("=" * 50 + "\n")
What's new in this function?
The function signature now accepts a warnings parameter (the list from check_thresholds()).
Three status variables check each metric against its threshold and produce either [WARNING] or an empty string. These get appended to the end of each print line.
The alerts section at the bottom either lists every warning, or prints a reassuring "All systems healthy" message.
- Save health_monitor.py.
Seeing a NameError for THRESHOLDS?
Make sure the THRESHOLDS dictionary is defined at the top of the file (below the imports, above all functions). If it's inside a function, other functions can't access it.
Help me fix a NameError in my health monitor script.
Now you need to update the __main__ block at the bottom of the file so it calls check_thresholds() and passes the warnings to display_metrics().
- Scroll to the bottom of health_monitor.py. Find this code:
if __name__ == "__main__":
metrics = get_system_metrics()
display_metrics(metrics)
- Replace it with this updated version:
if __name__ == "__main__":
metrics = get_system_metrics()
warnings = check_thresholds(metrics)
display_metrics(metrics, warnings)
- Save health_monitor.py.
✔️ 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
#!/usr/bin/env python3
"""
System Health Monitor
Monitors CPU, memory, and disk usage with threshold-based alerting.
Logs all metrics to a file and displays a summary in the terminal.
"""
import psutil
from datetime import datetime
# Configuration
THRESHOLDS = {
"cpu": 80.0,
"memory": 85.0,
"disk": 90.0,
}
def get_system_metrics():
"""Collect system metrics including CPU, memory, and disk usage."""
cpu_usage = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
memory_usage = memory.percent
disk = psutil.disk_usage("/")
disk_usage = disk.percent
return {
"cpu": cpu_usage,
"memory": memory_usage,
"disk": disk_usage,
"memory_total_gb": round(memory.total / (1024**3), 2),
"memory_used_gb": round(memory.used / (1024**3), 2),
"disk_total_gb": round(disk.total / (1024**3), 2),
"disk_used_gb": round(disk.used / (1024**3), 2),
}
def check_thresholds(metrics):
"""Check metrics against thresholds and return a list of warnings."""
warnings = []
for metric_name, threshold in THRESHOLDS.items():
value = metrics.get(metric_name, 0)
if value > threshold:
warnings.append(
f"{metric_name.upper()} usage is {value:.1f}% "
f"(threshold: {threshold:.1f}%)"
)
return warnings
def display_metrics(metrics, warnings):
"""Display a formatted health report in the terminal."""
print("\n" + "=" * 50)
print(" SYSTEM HEALTH REPORT")
print("=" * 50)
print(f" Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("-" * 50)
cpu_status = " [WARNING]" if metrics["cpu"] > THRESHOLDS["cpu"] else ""
mem_status = " [WARNING]" if metrics["memory"] > THRESHOLDS["memory"] else ""
disk_status = " [WARNING]" if metrics["disk"] > THRESHOLDS["disk"] else ""
print(f" CPU Usage: {metrics['cpu']:.1f}%{cpu_status}")
print(
f" Memory Usage: {metrics['memory']:.1f}% "
f"({metrics['memory_used_gb']} / {metrics['memory_total_gb']} GB){mem_status}"
)
print(
f" Disk Usage: {metrics['disk']:.1f}% "
f"({metrics['disk_used_gb']} / {metrics['disk_total_gb']} GB){disk_status}"
)
print("-" * 50)
if warnings:
print(" ALERTS:")
for warning in warnings:
print(f" ⚠ {warning}")
else:
print(" Status: All systems healthy ✓")
print("=" * 50 + "\n")
if __name__ == "__main__":
metrics = get_system_metrics()
warnings = check_thresholds(metrics)
display_metrics(metrics, warnings)
Run the script and test your alerts
Your alerting system is wired up. Time to run the script and see it in action.
- Run your health monitor by entering this command in the terminal:
python3 health_monitor.py
You should see the health report with a "Status: All systems healthy" message at the bottom (assuming your machine isn't under heavy load).
What if my metrics are all below the thresholds?
That's expected. On a typical idle machine, CPU sits well below 80% and memory is usually under 85%. The thresholds are set at levels that indicate real problems.
To prove the warnings actually work, you'll temporarily lower the thresholds in the next few bullets.
- In health_monitor.py, find the THRESHOLDS dictionary. Temporarily change the values to something very low:
# Configuration
THRESHOLDS = {
"cpu": 5.0,
"memory": 5.0,
"disk": 5.0,
}
- Save health_monitor.py and run the script again:
python3 health_monitor.py
You should now see [WARNING] labels next to each metric and an ALERTS section listing all three violations.
Still seeing "All systems healthy"?
Make sure you saved the file after changing the threshold values to 5.0. Also check that the check_thresholds() call appears in the __main__ block before display_metrics().
Help me debug why my threshold warnings aren't appearing.
- Now change the thresholds back to the production values:
# Configuration
THRESHOLDS = {
"cpu": 80.0,
"memory": 85.0,
"disk": 90.0,
}
- Save health_monitor.py.
Your monitor now knows the difference between a healthy system and one that needs attention. Next up, you'll add logging so every health check gets recorded to a file with timestamps.
Log Metrics to a File
Your health monitor now collects metrics and flags anything that crosses a threshold. The problem is that all of this output disappears the moment you close your terminal.
In production, DevOps engineers need a permanent record of every health check. Python's built-in logging module solves this by writing timestamped entries to a file. In this step, you'll wire up logging so every metric reading and every warning gets saved to a log file automatically.
In this step, get ready to:
- Configure Python's logging module with a file handler and timestamps.
- Log every metric reading and threshold violation to a file.
- Verify the log file accumulates entries across multiple runs.
Configure the logging module
Python's logging module gives you structured output with timestamps, severity levels, and the ability to write to multiple destinations at once. You'll configure it to write to both a log file and your terminal.
- In health_monitor.py, find the import section at the top of the file:
import psutil
from datetime import datetime
- Add import logging and from pathlib import Path so the imports look like this:
import psutil
import logging
from pathlib import Path
from datetime import datetime
- Now scroll down to the THRESHOLDS dictionary. Add the following logging configuration directly below it by copying this code:
LOG_FILE = Path("system_health.log")
# Set up logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler(LOG_FILE),
logging.StreamHandler(),
],
)
logger = logging.getLogger(__name__)
What does this code do?
- LOG_FILE = Path("system_health.log") defines the log file path using pathlib, Python's modern file path library.
- logging.basicConfig() sets up the root logger with INFO level (captures everything at INFO severity and above).
- The format string gives each entry a timestamp, severity label (INFO/WARNING), and the message.
- Two handlers mean every log entry goes to both the file AND your terminal simultaneously.
- logging.getLogger(__name__) creates a named logger for your module, which is the recommended pattern for production Python.
- Save the file.
Seeing a syntax error?
Make sure the LOG_FILE line and the logging.basicConfig() block sit between the THRESHOLDS dictionary and the def get_system_metrics() function. They should not be indented inside any function.
help me fix a logging configuration error in my Python script
Log every metric reading and threshold violation
The logger is configured, but nothing is calling it yet. You need to update the main execution block so it sends metric data and warnings through the logger.
- Scroll to the bottom of health_monitor.py and find the if __name__ == "__main__": block:
if __name__ == "__main__":
metrics = get_system_metrics()
warnings = check_thresholds(metrics)
display_metrics(metrics, warnings)
- Replace the entire if __name__ == "__main__": block with this updated version:
if __name__ == "__main__":
logger.info("Starting system health check")
metrics = get_system_metrics()
warnings = check_thresholds(metrics)
# Log the collected metrics
logger.info(
f"CPU: {metrics['cpu']:.1f}% | "
f"Memory: {metrics['memory']:.1f}% | "
f"Disk: {metrics['disk']:.1f}%"
)
# Log any threshold violations as warnings
for warning in warnings:
logger.warning(warning)
display_metrics(metrics, warnings)
logger.info("Health check complete")
What does this code do?
- logger.info() writes an INFO-level entry. You use it for routine data like metric readings and start/stop markers.
- logger.warning() writes a WARNING-level entry. You use it for threshold violations so they stand out in the log file.
- The f-string inside logger.info() formats all three metrics into one readable line. This makes log files easy to scan at a glance.
- Save the file.
Seeing a NameError for logger?
This means the logger = logging.getLogger(__name__) line from the previous substep is missing or placed inside a function. It must be at the module level (no indentation) above the function definitions.
help me fix a NameError for logger in my Python health monitor
Run the script and verify the log file
Time to confirm that log entries are being written to disk. You'll run the script a few times, then inspect the log file to see accumulated entries.
- Run the health monitor by running this command:
python3 health_monitor.py
You should see your terminal output now includes timestamped log lines above the health report. These same lines are also being saved to a file.
- Run it a second time to prove entries accumulate:
python3 health_monitor.py
- View the log file contents by running this command:
cat system_health.log
What should I see?
You should see multiple timestamped entries from both runs. Each run produces a "Starting system health check" line, a metrics line with CPU/Memory/Disk percentages, and a "Health check complete" line. If any thresholds were breached, you'll also see WARNING lines.
On Windows, use type system_health.log instead of cat.
Log file is empty or missing?
Check that LOG_FILE = Path("system_health.log") is at the module level (not inside a function). Also confirm that logging.FileHandler(LOG_FILE) is in the handlers list inside logging.basicConfig().
help me figure out why my system_health.log file is empty or not being created
✔️ Awesome, I've got everything!
Great. Double check you've saved health_monitor.py and that your system_health.log file shows multiple timestamped entries.
ⓧ I'd like to double check the full code
Here is the complete health_monitor.py at this point:
#!/usr/bin/env python3
"""
System Health Monitor
Monitors CPU, memory, and disk usage with threshold-based alerting.
Logs all metrics to a file and displays a summary in the terminal.
"""
import psutil
import logging
from pathlib import Path
from datetime import datetime
# Configuration
THRESHOLDS = {
"cpu": 80.0,
"memory": 85.0,
"disk": 90.0,
}
LOG_FILE = Path("system_health.log")
# Set up logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler(LOG_FILE),
logging.StreamHandler(),
],
)
logger = logging.getLogger(__name__)
def get_system_metrics():
"""Collect system metrics including CPU, memory, and disk usage."""
cpu_usage = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
memory_usage = memory.percent
disk = psutil.disk_usage("/")
disk_usage = disk.percent
return {
"cpu": cpu_usage,
"memory": memory_usage,
"disk": disk_usage,
"memory_total_gb": round(memory.total / (1024**3), 2),
"memory_used_gb": round(memory.used / (1024**3), 2),
"disk_total_gb": round(disk.total / (1024**3), 2),
"disk_used_gb": round(disk.used / (1024**3), 2),
}
def check_thresholds(metrics):
"""Check metrics against thresholds and return a list of warnings."""
warnings = []
for metric_name, threshold in THRESHOLDS.items():
value = metrics.get(metric_name, 0)
if value > threshold:
warnings.append(
f"{metric_name.upper()} usage is {value:.1f}% "
f"(threshold: {threshold:.1f}%)"
)
return warnings
def display_metrics(metrics, warnings):
"""Display a formatted health report in the terminal."""
print("\n" + "=" * 50)
print(" SYSTEM HEALTH REPORT")
print("=" * 50)
print(f" Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("-" * 50)
cpu_status = " [WARNING]" if metrics["cpu"] > THRESHOLDS["cpu"] else ""
mem_status = " [WARNING]" if metrics["memory"] > THRESHOLDS["memory"] else ""
disk_status = " [WARNING]" if metrics["disk"] > THRESHOLDS["disk"] else ""
print(f" CPU Usage: {metrics['cpu']:.1f}%{cpu_status}")
print(
f" Memory Usage: {metrics['memory']:.1f}% "
f"({metrics['memory_used_gb']} / {metrics['memory_total_gb']} GB){mem_status}"
)
print(
f" Disk Usage: {metrics['disk']:.1f}% "
f"({metrics['disk_used_gb']} / {metrics['disk_total_gb']} GB){disk_status}"
)
print("-" * 50)
if warnings:
print(" ALERTS:")
for warning in warnings:
print(f" ⚠ {warning}")
else:
print(" Status: All systems healthy ✓")
print("=" * 50 + "\n")
if __name__ == "__main__":
logger.info("Starting system health check")
metrics = get_system_metrics()
warnings = check_thresholds(metrics)
logger.info(
f"CPU: {metrics['cpu']:.1f}% | "
f"Memory: {metrics['memory']:.1f}% | "
f"Disk: {metrics['disk']:.1f}%"
)
for warning in warnings:
logger.warning(warning)
display_metrics(metrics, warnings)
logger.info("Health check complete")
Your monitor now keeps a permanent record of every health check. Next up, you'll use Python's subprocess module to run a real system command and pull your machine's uptime directly into the report.
Run a System Command with subprocess
Your health monitor collects metrics through psutil, but real DevOps scripts also need to run system commands directly. The uptime command tells you how long a machine has been running without a restart, which is critical context when diagnosing performance issues.
Python's subprocess module lets you run any terminal command from within your script and capture its output. In this step, you'll use it to fetch your system's uptime and integrate it into the health report.
In this step, get ready to:
- Write a function that executes a system command and captures its output.
- Integrate uptime information into your health report display and log.
- Run the completed script end-to-end and review the full output.
Add a get_uptime() function using subprocess.run()
The subprocess module is Python's standard way to run shell commands. You'll use subprocess.run() with capture_output=True and text=True to get the command's output as a Python string.
- In health_monitor.py, find the import lines at the top of your file.
- Find this line:
import psutil
- Add import subprocess and import platform directly below it, so the imports section looks like this:
import psutil
import subprocess
import logging
import platform
from pathlib import Path
from datetime import datetime
Why do we need platform?
The command to check uptime differs between operating systems. macOS and Linux use the uptime command, while Windows uses net stats workstation. The platform module lets your script detect which OS it's running on and choose the right command.
- Now scroll down to find the check_thresholds() function's closing line.
- Add the following get_uptime() function below check_thresholds() and above display_metrics():
def get_uptime():
"""Get system uptime using a subprocess call."""
try:
if platform.system() == "Windows":
result = subprocess.run(
["net", "stats", "workstation"],
capture_output=True,
text=True,
check=True,
)
for line in result.stdout.splitlines():
if "Statistics since" in line:
return line.strip()
return "Uptime information unavailable"
else:
result = subprocess.run(
["uptime"],
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()
except (subprocess.CalledProcessError, FileNotFoundError) as e:
return f"Could not retrieve uptime: {e}"
What does this code do?
This function runs a system command and returns its output as a string:
- platform.system() detects whether you're on Windows, Linux, or macOS (returned as "Darwin" for macOS).
- subprocess.run() executes the command. The capture_output=True flag captures whatever the command prints. The text=True flag returns the output as a string instead of raw bytes.
- check=True raises a CalledProcessError if the command fails, which the except block catches gracefully.
- The function also catches FileNotFoundError in case the command doesn't exist on the system.
- Save health_monitor.py.
Seeing a syntax error?
- Make sure the try: block is indented one level inside the function body.
- Check that import subprocess and import platform are at the top of the file, not inside a function.
Why is my get_uptime() function throwing an error?
Integrate uptime into the health report display and log
Now you need to update display_metrics() to show the uptime, and reorganize the script's logic into a clean main() function.
- In health_monitor.py, find the display_metrics function definition line:
def display_metrics(metrics, warnings):
- Replace it with this updated signature that accepts an uptime parameter:
def display_metrics(metrics, warnings, uptime):
- Next, find this line inside display_metrics():
print(f" Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
- Add this line directly below it:
print(f" Uptime: {uptime}")
- Now scroll to the bottom of the file. Find the existing if __name__ == "__main__": block and replace it entirely with this main() function and updated entry point:
def main():
"""Main function to collect and report system health."""
logger.info("Starting system health check")
metrics = get_system_metrics()
warnings = check_thresholds(metrics)
uptime = get_uptime()
# Log metrics
logger.info(
f"CPU: {metrics['cpu']:.1f}% | "
f"Memory: {metrics['memory']:.1f}% | "
f"Disk: {metrics['disk']:.1f}%"
)
# Log any warnings
for warning in warnings:
logger.warning(warning)
# Display the report
display_metrics(metrics, warnings, uptime)
logger.info("Health check complete")
if __name__ == "__main__":
main()
What does main() do?
The main() function orchestrates everything in order: collect metrics, check thresholds, get uptime, log results, and display the report.
Wrapping your logic in a main() function is a Python best practice. It keeps the global scope clean and makes it possible to import your script's functions in other files without triggering the full health check.
- Save health_monitor.py.
Getting a TypeError about arguments?
- If you see a message about display_metrics() receiving an unexpected argument, make sure you updated the function signature to accept three parameters: metrics, warnings, and uptime.
- Check that the main() function is defined at the module level (no indentation), not inside another function.
Why am I getting a TypeError in display_metrics?
Run the final script and review the output
Your script is now complete. Time to run it and see everything working together.
- Run the health monitor by executing this command in your terminal:
python3 health_monitor.py
- You should see a complete health report in your terminal with a header, timestamp, uptime, CPU/memory/disk usage, and a status line.
- The Uptime: line now appears below the timestamp, showing how long your machine has been running.
What should I see?
Your terminal output will look something like this (with your own system's values):
==================================================
SYSTEM HEALTH REPORT
==================================================
Timestamp: 2026-06-03 14:30:00
Uptime: 14:30 up 3 days, 2:15, 2 users, load averages: 1.45 2.10 1.89
--------------------------------------------------
CPU Usage: 12.5%
Memory Usage: 62.3% (10.24 / 16.0 GB)
Disk Usage: 45.2% (214.5 / 476.0 GB)
--------------------------------------------------
Status: All systems healthy ✓
==================================================
On Windows, the uptime line will show a date like Statistics since 6/1/2026 9:00:00 AM instead.
- Now verify that the log file is also recording your runs. View it by running:
cat system_health.log
On Windows, use type instead
If you're on Windows, run type system_health.log instead of cat.
- You should see timestamped entries including "Starting system health check", the CPU/Memory/Disk percentages, and "Health check complete".
Don't see the uptime line?
- Make sure you added print(f" Uptime: {uptime}") directly below the Timestamp print line inside display_metrics().
- Confirm that main() calls get_uptime() and passes the result to display_metrics().
- If the uptime line says "Could not retrieve uptime", the command may not be available on your system. The script handles this gracefully and still works.
Why isn't my uptime showing in the report?
✔️ Awesome, I've got everything!
Great. Double check you've saved health_monitor.py and confirmed both the terminal report and log file look correct.
ⓧ I'd like to double check the full code
Here is the complete health_monitor.py file at this point:
#!/usr/bin/env python3
"""
System Health Monitor
Monitors CPU, memory, and disk usage with threshold-based alerting.
Logs all metrics to a file and displays a summary in the terminal.
"""
import psutil
import subprocess
import logging
import platform
from pathlib import Path
from datetime import datetime
# Configuration
THRESHOLDS = {
"cpu": 80.0,
"memory": 85.0,
"disk": 90.0,
}
LOG_FILE = Path("system_health.log")
# Set up logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler(LOG_FILE),
logging.StreamHandler(),
],
)
logger = logging.getLogger(__name__)
def get_system_metrics():
"""Collect system metrics including CPU, memory, and disk usage."""
cpu_usage = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
memory_usage = memory.percent
disk = psutil.disk_usage("/")
disk_usage = disk.percent
return {
"cpu": cpu_usage,
"memory": memory_usage,
"disk": disk_usage,
"memory_total_gb": round(memory.total / (1024**3), 2),
"memory_used_gb": round(memory.used / (1024**3), 2),
"disk_total_gb": round(disk.total / (1024**3), 2),
"disk_used_gb": round(disk.used / (1024**3), 2),
}
def check_thresholds(metrics):
"""Check metrics against thresholds and return a list of warnings."""
warnings = []
for metric_name, threshold in THRESHOLDS.items():
value = metrics.get(metric_name, 0)
if value > threshold:
warnings.append(
f"{metric_name.upper()} usage is {value:.1f}% "
f"(threshold: {threshold:.1f}%)"
)
return warnings
def get_uptime():
"""Get system uptime using a subprocess call."""
try:
if platform.system() == "Windows":
result = subprocess.run(
["net", "stats", "workstation"],
capture_output=True,
text=True,
check=True,
)
for line in result.stdout.splitlines():
if "Statistics since" in line:
return line.strip()
return "Uptime information unavailable"
else:
result = subprocess.run(
["uptime"],
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()
except (subprocess.CalledProcessError, FileNotFoundError) as e:
return f"Could not retrieve uptime: {e}"
def display_metrics(metrics, warnings, uptime):
"""Display a formatted health report in the terminal."""
print("\n" + "=" * 50)
print(" SYSTEM HEALTH REPORT")
print("=" * 50)
print(f" Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f" Uptime: {uptime}")
print("-" * 50)
cpu_status = " [WARNING]" if metrics["cpu"] > THRESHOLDS["cpu"] else ""
mem_status = " [WARNING]" if metrics["memory"] > THRESHOLDS["memory"] else ""
disk_status = " [WARNING]" if metrics["disk"] > THRESHOLDS["disk"] else ""
print(f" CPU Usage: {metrics['cpu']:.1f}%{cpu_status}")
print(
f" Memory Usage: {metrics['memory']:.1f}% "
f"({metrics['memory_used_gb']} / {metrics['memory_total_gb']} GB){mem_status}"
)
print(
f" Disk Usage: {metrics['disk']:.1f}% "
f"({metrics['disk_used_gb']} / {metrics['disk_total_gb']} GB){disk_status}"
)
print("-" * 50)
if warnings:
print(" ALERTS:")
for warning in warnings:
print(f" ⚠ {warning}")
else:
print(" Status: All systems healthy ✓")
print("=" * 50 + "\n")
def main():
"""Main function to collect and report system health."""
logger.info("Starting system health check")
metrics = get_system_metrics()
warnings = check_thresholds(metrics)
uptime = get_uptime()
# Log metrics
logger.info(
f"CPU: {metrics['cpu']:.1f}% | "
f"Memory: {metrics['memory']:.1f}% | "
f"Disk: {metrics['disk']:.1f}%"
)
# Log any warnings
for warning in warnings:
logger.warning(warning)
# Display the report
display_metrics(metrics, warnings, uptime)
logger.info("Health check complete")
if __name__ == "__main__":
main()
Secret mission
Add Network Monitoring and JSON Export
Your health monitor tracks CPU, memory, and disk. But what about network activity? Extend your script with network I/O stats and export everything to a machine-readable JSON report file that other tools can consume.
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, so there are no ongoing costs. The only resources on your machine are the project folder and its contents.
Resources you used:
- Project folder on your Desktop containing health_monitor.py, system_health.log, and health_report.json.
- Python virtual environment (venv folder) with psutil installed.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep running the health monitor.
- Your project folder stays on your Desktop with all files intact.
- You can re-run the script anytime by activating the virtual environment and running python health_monitor.py.
- The system_health.log file will keep accumulating entries each time you run the script.
Pause - I'll come back to this later
Deactivate your virtual environment to free up your terminal session. Your files stay on disk and are ready to pick back up.
- Deactivate the virtual environment by running:
deactivate
- When you come back, navigate to your project folder and reactivate with source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows).
Delete - I don't want to use this again
Remove all project files and the virtual environment from your machine. This is permanent.
- First, deactivate the virtual environment if it's still active:
deactivate
- Navigate to your Desktop:
cd ~/Desktop
- Delete the entire project folder (macOS/Linux):
rm -rf health-monitor
- On Windows, use this command instead:
rmdir /s /q health-monitor
- This removes health_monitor.py, system_health.log, health_report.json, and the venv folder with all installed packages.
Nice Work!
Nice Work!
You just built a real DevOps automation tool from scratch. Your system health monitor collects live metrics, flags problems, logs everything with timestamps, and runs system commands. This is the exact pattern used in production monitoring scripts at companies running their own infrastructure.
Here's what you learned:
- Collected live system metrics (CPU, memory, disk) using the psutil library and displayed them in a clean terminal report.
- Implemented threshold-based alerting that compares metrics against configurable limits and flags unhealthy resources with warnings.
- Used Python's logging module to create production-grade log files and subprocess to run system commands directly from Python.
- Added network monitoring and a JSON export function for machine-readable reports.
Ready to quiz yourself?