Build a Linux Health Monitor in Bash
Build a Bash monitor that reports Linux health data and saves reusable logs.
Introduction
30 Second Summary
A computer can stay online while problems quietly build beneath the surface. Without a quick snapshot, the warning signs are easy to miss.
In this project, you will build linux-health.sh, a zero-install terminal health monitor in Bash. It reads Linux system data to produce a reusable health snapshot.
What You'll Build
Running linux-health.sh gives you a compact terminal report that shows whether your Linux server needs attention.
By the end of this project, you'll have:
- A resource snapshot showing uptime, load averages, task counts, and available memory.
- A diagnostic view identifying a target process, the root filesystem, and every network interface.
- An automation-ready result saved to linux-health.log, with exit statuses that signal healthy output, warnings, or invalid input.
- Secret Mission: Replace positional arguments with friendly named options for selecting the target process, memory threshold, and network interface.
Are there any prerequisites?
You need access to an existing Linux VM or server with Bash. No package installation, WSL installation, or Windows administrator access is required.
Before We Start
Your choice of Linux server gives the health monitor a clear target. The reason you need a reusable Bash snapshot gives the project a practical purpose.
Verify Your Bash Environment
Your health monitor has to work on the Linux server you already chose. This setup confirms that the host can support the script without extra packages.
Bash provides the arithmetic, conditions, formatted output, and redirection used throughout the monitor. Linux kernel paths provide the live data that later steps turn into a health snapshot.
In this step, get ready to:
- Connect to your existing Linux VM or server and confirm Bash is available.
- Test the Bash features and Linux kernel paths required by the monitor.
- Create a runnable placeholder that prints a readiness message.
Connect and confirm Bash
The monitor runs inside the shell on your Linux host. Confirming the Bash executable first prevents every later check from depending on an untested environment.
- Connect to the Linux VM or server you selected using its existing connection method.
- Confirm that you see a shell prompt for that host.
- Display Bash version information by running this command:
bash --version
What does this command do?
The version option asks Bash to display its version information. A successful result confirms that the Bash executable is available in your shell.
You should see version information for Bash near the top of the output.
- Select the tab that matches your result.
✔️ Bash reports version information
Good start. Your Linux host can run Bash commands for the health monitor.
ⓧ Bash is unavailable
This project requires a Linux host where Bash is already available. Package installation is outside this setup.
- Switch to a Linux VM or server that already provides Bash.
- Ask the server administrator to make Bash available if this is the host you must use.
Test shell features and kernel access
The monitor reads system information through procfs and sysfs. A feature test also proves that this Bash environment supports the calculations and conditions used by the final script.
- Feature-test Bash arithmetic and conditional evaluation by running this command:
bash -c 'value=$(( 2 + 3 )); if [[ "$value" -eq 5 ]]; then printf "%s\n" "Bash feature test passed"; fi'
What does this feature test prove?
- The command starts a fresh Bash process for the feature check.
- Arithmetic expansion calculates the value used by the test.
- The conditional prints the success message only when that calculation equals the expected value.
You should see Bash feature test passed in the terminal.
- Check every required kernel path for readable access by running this command:
for path in \
/proc/meminfo \
/proc/loadavg \
/proc/uptime \
/proc/self/status \
/proc/self/mountinfo \
/sys/class/net
do
[[ -r "$path" ]] || exit 1
printf '%s\n' "$path"
done
What does this path check prove?
- The loop checks each required file or directory for readable access.
- The command stops with a non-zero status if the shell cannot read a required path.
- Printing all six paths gives you visible proof that the monitor can reach its future data sources.
You should see /proc/meminfo, /proc/loadavg, /proc/uptime, /proc/self/status, /proc/self/mountinfo, and /sys/class/net printed on separate lines.
Did the feature or path check stop early?
A missing success message means the Bash feature check did not complete as expected. A path list with fewer than six entries means the shell could not read one of the required kernel interfaces.
Use a Linux host that provides the required Bash features and readable kernel paths. Help me identify why my Bash feature or kernel path check stopped early.
Create and run the placeholder
Output redirection sends formatted terminal output into a file. A one-line placeholder proves that your shell can create the project script and execute it with Bash.
- Create linux-health.sh in your current directory by running this command:
printf '%s\n' "Health monitor workspace ready" > linux-health.sh
What does this command do?
- The printf builtin formats the readiness message with a newline.
- The redirection operator writes that output into linux-health.sh.
- The resulting file contains one runnable Bash command.
- Before you run the placeholder, predict whether Bash prints the line you wrote.
- Test your prediction by running this command:
bash linux-health.sh
What does this command do?
Bash reads the commands stored in linux-health.sh. The placeholder then prints its formatted message to the terminal.
You should see Health monitor workspace ready. That result confirms your project file is saved and runnable.
Missing the readiness message?
- Check that the filename is exactly linux-health.sh in both commands.
- Return to the same shell session where you created the file.
- Run the creation command again if the file was written in a different directory.
Help me find why my Bash placeholder does not print its readiness message.
✔️ Awesome, I've got everything!
Your placeholder is saved and runs successfully. The workspace is ready for the first health snapshot.
ⓧ I'd like to double check the full code
Your complete linux-health.sh file should contain this single line.
printf '%s\n' "Health monitor workspace ready"
Your shell features and Linux kernel interfaces are ready. Next, you will replace the placeholder with a resource snapshot that reads live data from the host.
Build a Resource Snapshot
Your placeholder proves that Bash can execute a local script on your server. Now the monitor needs useful numbers from the Linux kernel.
A visible resource baseline gives you a quick view of the server's current condition. In this step, you will turn procfs data into uptime, load, task, and memory readings.
In this step, get ready to:
- Read uptime, load averages, and task counts from procfs.
- Calculate a first-pass free-memory percentage with Bash arithmetic.
- Print a resource snapshot in your terminal.
Why read procfs directly?
Bash builtins can read Linux kernel interfaces directly. This keeps your monitor independent of extra packages.
External system utilities offer formatted views. Direct reads keep the focus on Bash parsing, control flow, and arithmetic.
Read uptime and load data
The /proc/loadavg file provides three load averages plus runnable and total task counts. The /proc/uptime file provides the time since the server started.
- Use the terminal editor available on your Linux server to open linux-health.sh from the current working directory.
- Replace the placeholder line with this first snapshot section:
read -r load_1 load_5 load_15 task_counts last_pid < /proc/loadavg
read -r uptime_value idle_value < /proc/uptime
printf 'Uptime: %s seconds\n' "${uptime_value%%.*}"
printf 'Load averages: %s %s %s\n' "$load_1" "$load_5" "$load_15"
printf 'Tasks: %s\n' "$task_counts"
What does this code do?
- The first read -r command splits the load data across load_1, load_5, load_15, task_counts, and last_pid.
- The second read -r command stores the uptime value in uptime_value.
- The parameter expansion ${uptime_value%%.*} removes the decimal portion before printing the uptime.
- Each printf call gives one resource category a readable label.
- Save linux-health.sh in your terminal editor.
- Run the first snapshot by entering this command in your shell:
bash linux-health.sh
What does this command do?
The bash executable reads the commands in linux-health.sh. Your script then prints the values it read from the kernel interfaces.
You should see lines labelled Uptime, Load averages, and Tasks. That is the first live slice of your server health monitor.
Missing a resource line?
- Confirm that the original placeholder line was fully replaced.
- Check that each input redirection points to the matching procfs path.
- Compare every variable name in the read commands with the matching printf argument.
Help me troubleshoot the missing resource output.
Calculate free memory
The /proc/meminfo file lists memory measurements as labelled rows. Your first calculation uses MemTotal and MemFree to produce a percentage.
- Add this memory parser at the top of linux-health.sh before the first read -r load_1 line:
mem_total_kb=0
mem_free_kb=0
while read -r key value unit; do
case "$key" in
MemTotal:) mem_total_kb="$value" ;;
MemFree:) mem_free_kb="$value" ;;
esac
done < /proc/meminfo
How does the memory parser work?
- The variables start at 0 before the parser reads the file.
- The while loop separates each row into key, value, and unit.
- The case command saves only the MemTotal: and MemFree: values.
- Input redirection feeds /proc/meminfo into the loop without requiring another program.
- Replace the existing uptime, load, and output section with this expanded section:
read -r load_1 load_5 load_15 task_counts last_pid < /proc/loadavg
read -r uptime_value idle_value < /proc/uptime
free_percent=$(( mem_free_kb * 100 / mem_total_kb ))
printf 'Uptime: %s seconds\n' "${uptime_value%%.*}"
printf 'Load averages: %s %s %s\n' "$load_1" "$load_5" "$load_15"
printf 'Tasks: %s\n' "$task_counts"
printf 'Memory: %s%% free\n' "$free_percent"
What does the calculation do?
- The arithmetic expression multiplies mem_free_kb by 100 before dividing by mem_total_kb.
- Bash arithmetic produces the whole-number percentage stored in free_percent.
- The final printf call escapes the percentage symbol with %%.
- Save the expanded linux-health.sh file.
Before you run the expanded script, what kind of free-memory percentage do you expect on this server? Hold that prediction in mind.
- Run the expanded resource snapshot with this command:
bash linux-health.sh
What does this run prove?
This run proves that the memory parser found both required fields. It also proves that Bash completed the percentage calculation.
You should now see a fourth line labelled Memory with a whole-number percentage. The value comes from the current MemFree reading.
Why is this a first-pass signal?
The percentage uses only the MemFree field. It leaves out other memory that the kernel could make available for applications.
That limitation is intentional in this first snapshot. You now have a visible baseline that you can challenge with a more meaningful memory signal.
Memory line missing or invalid?
- Check that the memory parser appears before the free_percent calculation.
- Confirm that MemTotal: includes its trailing colon in the case pattern.
- Confirm that MemFree: includes its trailing colon in the case pattern.
Help me debug the memory calculation.
Check the complete snapshot
Your script now turns three kernel interfaces into one readable terminal snapshot. Compare the complete file before the final run.
✔️ Awesome, I've got everything!
Great. Your first-pass resource snapshot is complete and saved.
ⓧ I'd like to double check the full code
- Compare your complete linux-health.sh file with this reference:
mem_total_kb=0
mem_free_kb=0
while read -r key value unit; do
case "$key" in
MemTotal:) mem_total_kb="$value" ;;
MemFree:) mem_free_kb="$value" ;;
esac
done < /proc/meminfo
read -r load_1 load_5 load_15 task_counts last_pid < /proc/loadavg
read -r uptime_value idle_value < /proc/uptime
free_percent=$(( mem_free_kb * 100 / mem_total_kb ))
printf 'Uptime: %s seconds\n' "${uptime_value%%.*}"
printf 'Load averages: %s %s %s\n' "$load_1" "$load_5" "$load_15"
printf 'Tasks: %s\n' "$task_counts"
printf 'Memory: %s%% free\n' "$free_percent"
Before the final check, which readings do you expect to change between runs? Keep your prediction in mind.
- Verify the complete resource snapshot by running:
bash linux-health.sh
What does the final check confirm?
The command runs the saved file from top to bottom. A complete result confirms that all three procfs reads feed the matching output lines.
You should see Uptime, Load averages, Tasks, and Memory lines. The task line includes runnable and total counts separated by /.
That first resource snapshot is working. Next, you will test whether its memory number represents useful headroom and turn the result into an actionable warning.
Add Memory Warning Logic
Your first resource snapshot turned procfs data into a readable memory percentage. However, MemFree counts only completely unused RAM.
Your Bash monitor now needs a more useful signal. MemAvailable estimates how much memory can support new applications without swapping.
In this step, get ready to:
- Accept a positional memory threshold with a safe default.
- Replace MemFree with the kernel's MemAvailable estimate.
- Return an automation-friendly warning status when available memory falls below the threshold.
Add positional defaults and threshold validation
A configurable threshold lets the same monitor suit servers with different memory needs. The script uses a default threshold of 20 when no second positional value is supplied.
- In linux-health.sh, place your cursor above mem_total_kb=0.
- Add the positional defaults and threshold validation by pasting this block:
target_pid="${1:-$$}"
memory_warning_percent="${2:-20}"
case "$memory_warning_percent" in
''|*[!0-9]*) exit 2 ;;
esac
if (( memory_warning_percent < 1 || memory_warning_percent > 100 )); then
exit 2
fi
How does the validation work?
- The first positional value becomes target_pid. Its default is $$, which represents the current shell process.
- The second positional value becomes memory_warning_percent. Its default is 20.
- The case pattern rejects empty values or values containing non-numeric characters.
- The arithmetic condition accepts integers from 1 through 100. Invalid input exits with status 2.
- Save linux-health.sh.
- Confirm the default values pass validation by running this command:
bash linux-health.sh
What does this check prove?
The existing resource snapshot continues to run when no arguments are supplied. This confirms that the default threshold passes the new validation.
Good work. Your monitor now accepts a threshold without breaking its existing snapshot.
Does the script exit immediately?
- Confirm the new block sits above mem_total_kb=0.
- Check that the numeric pattern is exactly ''|*[!0-9]*.
- Compare both arithmetic boundaries with 1 and 100.
Help me find why my threshold validation exits early.
Use available memory as the signal
Linux can reclaim some memory that does not appear in MemFree. The MemAvailable field accounts for that reclaimable capacity, which makes it a better measure of memory headroom.
- Locate the memory block from mem_total_kb=0 through the free_percent calculation.
- Replace the entire memory block by pasting this code:
mem_total_kb=0
mem_available_kb=0
while read -r key value unit; do
case "$key" in
MemTotal:) mem_total_kb="$value" ;;
MemAvailable:) mem_available_kb="$value" ;;
esac
done < /proc/meminfo
available_percent=$(( mem_available_kb * 100 / mem_total_kb ))
used_percent=$(( 100 - available_percent ))
overall_status="OK"
exit_code=0
memory_status="OK"
if (( available_percent < memory_warning_percent )); then
memory_status="WARNING"
overall_status="WARNING"
exit_code=1
fi
What does this memory logic do?
- The loop reads MemTotal and MemAvailable from /proc/meminfo.
- The arithmetic calculates available memory as a percentage of total memory.
- The used percentage is the remaining share after subtracting available memory from 100.
- The condition changes the memory status and overall status when available memory falls below the configured threshold.
- The exit_code value remains 0 for a healthy result. A warning changes it to 1.
- Locate the output group from the uptime line through the old memory line.
- Replace that output group by pasting these lines:
printf 'Uptime: %s seconds\n' "${uptime_value%%.*}"
printf 'Load averages: %s %s %s\n' "$load_1" "$load_5" "$load_15"
printf 'Tasks: %s\n' "$task_counts"
printf 'Memory: %s%% available, %s%% used, threshold %s%% [%s]\n' "$available_percent" "$used_percent" "$memory_warning_percent" "$memory_status"
printf 'Overall status: %s\n' "$overall_status"
exit "$exit_code"
What does the updated output show?
- The memory line displays the available percentage and used percentage.
- The same line displays the configured threshold and current memory status.
- The overall status summarizes the result for a person reading the report.
- The final exit returns the result for another command or script to inspect.
- Save linux-health.sh.
Before you run the updated monitor, predict how the memory line changes when it uses available memory instead of completely free memory.
- Display the updated resource snapshot by running this command:
bash linux-health.sh
What does this run check?
The default run reads MemAvailable and compares it with the default threshold. It also returns the matching exit status after printing the snapshot.
You should see available memory and used memory on the same line. You should also see a threshold of 20% with either OK or WARNING based on your server's current memory headroom.
Is the memory percentage missing?
- Confirm every mem_free_kb reference was replaced with mem_available_kb.
- Check that the parser matches the MemAvailable: key from /proc/meminfo.
- Confirm the memory output uses available_percent and used_percent.
Help me debug my missing memory values.
Force a warning and inspect the exit status
An exit status gives automation a compact result after the human-readable report finishes. A threshold of 100 forces the monitor down its warning path for this check.
Before you run the check, predict whether the monitor remains healthy or moves to warning.
- Run the monitor with a memory threshold of 100 by using this command:
bash linux-health.sh '' 100; printf 'Exit: %s\n' "$?"
How does this verification work?
- The empty first argument keeps the default process ID.
- The second argument sets memory_warning_percent to 100.
- The monitor marks available memory below that threshold as a warning.
- The final printf displays the exit status returned by the script.
You should see WARNING on the memory line and overall status line. The final line should show Exit: 1.
✔️ Awesome, I've got everything!
Your monitor now reports meaningful memory headroom and returns a warning status when the threshold is crossed.
ⓧ I'd like to double check the full code
Compare your complete linux-health.sh file with this version:
target_pid="${1:-$$}"
memory_warning_percent="${2:-20}"
case "$memory_warning_percent" in
''|*[!0-9]*) exit 2 ;;
esac
if (( memory_warning_percent < 1 || memory_warning_percent > 100 )); then
exit 2
fi
mem_total_kb=0
mem_available_kb=0
while read -r key value unit; do
case "$key" in
MemTotal:) mem_total_kb="$value" ;;
MemAvailable:) mem_available_kb="$value" ;;
esac
done < /proc/meminfo
available_percent=$(( mem_available_kb * 100 / mem_total_kb ))
used_percent=$(( 100 - available_percent ))
overall_status="OK"
exit_code=0
memory_status="OK"
if (( available_percent < memory_warning_percent )); then
memory_status="WARNING"
overall_status="WARNING"
exit_code=1
fi
read -r load_1 load_5 load_15 task_counts last_pid < /proc/loadavg
read -r uptime_value idle_value < /proc/uptime
printf 'Uptime: %s seconds\n' "${uptime_value%%.*}"
printf 'Load averages: %s %s %s\n' "$load_1" "$load_5" "$load_15"
printf 'Tasks: %s\n' "$task_counts"
printf 'Memory: %s%% available, %s%% used, threshold %s%% [%s]\n' "$available_percent" "$used_percent" "$memory_warning_percent" "$memory_status"
printf 'Overall status: %s\n' "$overall_status"
exit "$exit_code"
This complete file matches the memory warning monitor you built in this step.
Your monitor now turns kernel memory data into a signal that people and scripts can use. Next up, you will add process details and root storage context.
Inspect Processes and Storage
Your Bash monitor can flag low memory. Resource totals still cannot identify the process under inspection or the storage backing the root filesystem.
Linux exposes this context through procfs. In this step, you will inspect the script's own process by default. You will also decode the mount record for /.
In this step, get ready to:
- Use the script's process ID as the default inspection target.
- Read a target process's status fields from procfs.
- Decode the root filesystem's mount record.
Inspect the target process
The /proc/PID/status interface provides human-readable details about a process. The $$ value makes the script inspect its own Bash process when you omit the first positional argument.
- Replace the contents of linux-health.sh with this process inspection checkpoint:
target_pid="${1:-$$}"
memory_warning_percent="${2:-20}"
overall_status="OK"
exit_code=0
mark_warning() {
overall_status="WARNING"
exit_code=1
}
process_readable=0
process_name="unknown"
process_state="unknown"
process_rss="unknown"
process_threads="unknown"
if [[ -r "/proc/$target_pid/status" ]]; then
process_readable=1
while read -r key value rest; do
case "$key" in
Name:) process_name="$value${rest:+ $rest}" ;;
State:) process_state="$value${rest:+ $rest}" ;;
VmRSS:) process_rss="$value${rest:+ $rest}" ;;
Threads:) process_threads="$value" ;;
esac
done < "/proc/$target_pid/status"
else
mark_warning
fi
printf 'Process %s: %s, state %s, RSS %s, threads %s\n' "$target_pid" "$process_name" "$process_state" "$process_rss" "$process_threads"
What does this code do?
- The target_pid value uses the first positional argument when one is supplied. Its default is the script's own process ID.
- The mark_warning() function updates the overall result when a check cannot be completed.
- The readability test confirms that the selected process has an accessible status file.
- The loop extracts the process name, state, resident memory, and thread count.
- Save linux-health.sh.
- Run the process checkpoint with Bash by using:
bash linux-health.sh
You should see a line beginning with Process. The line includes the script's PID plus its Bash process details.
Seeing unknown process details?
- Confirm that the path uses /proc/$target_pid/status exactly.
- Check that each field name retains its trailing colon inside the case statement.
Help me find why my process fields remain unknown.
Decode the root mount record
The /proc/self/mountinfo file describes the current process's mounted filesystems. The record for mount point / places the filesystem type plus mount source after the - separator.
- Find this process output line near the bottom of linux-health.sh:
printf 'Process %s: %s, state %s, RSS %s, threads %s\n' "$target_pid" "$process_name" "$process_state" "$process_rss" "$process_threads"
This line currently reports the process data without any storage context.
- Replace that line with the root mount parser plus both report lines below:
root_fs_type="unknown"
root_mount_source="unknown"
while read -r mount_id parent_id major_minor mount_root mount_point mount_options remaining; do
if [[ "$mount_point" == "/" ]]; then
filesystem_fields="${remaining#*- }"
read -r root_fs_type root_mount_source root_super_options <<< "$filesystem_fields"
break
fi
done < /proc/self/mountinfo
if [[ "$root_fs_type" == "unknown" ]]; then
mark_warning
fi
printf 'Process %s: %s, state %s, RSS %s, threads %s\n' "$target_pid" "$process_name" "$process_state" "$process_rss" "$process_threads"
printf 'Root storage: source %s, filesystem %s\n' "$root_mount_source" "$root_fs_type"
How does the mount parser work?
- The loop reads each mount record until mount_point equals /.
- The filesystem_fields expansion removes everything through the documented separator.
- The next read stores the filesystem type in root_fs_type. It stores the source in root_mount_source.
- A missing root record calls mark_warning() so automation can detect the incomplete check.
- Save linux-health.sh.
Before you run the completed monitor, predict which PID plus root filesystem record it will report.
- Run the completed process and storage checkpoint with Bash by using:
bash linux-health.sh
You should see the script's Bash PID with its name, state, resident memory, and thread count. You should also see the source plus filesystem type mounted at /.
That is a useful administration checkpoint. Your monitor now connects a selected process to the storage backing the root filesystem.
Missing the root storage details?
- Confirm that the loop reads from /proc/self/mountinfo.
- Check that the mount-point comparison uses /.
- Verify that filesystem_fields removes text through the separator before the second read.
Help me debug my root mount parser.
✔️ Awesome, I've got everything!
Your process plus root storage checks are saved. The script now prints both report lines when you run it.
ⓧ I'd like to double check the full code
Compare your linux-health.sh file with this checkpoint:
target_pid="${1:-$$}"
memory_warning_percent="${2:-20}"
overall_status="OK"
exit_code=0
mark_warning() {
overall_status="WARNING"
exit_code=1
}
process_readable=0
process_name="unknown"
process_state="unknown"
process_rss="unknown"
process_threads="unknown"
if [[ -r "/proc/$target_pid/status" ]]; then
process_readable=1
while read -r key value rest; do
case "$key" in
Name:) process_name="$value${rest:+ $rest}" ;;
State:) process_state="$value${rest:+ $rest}" ;;
VmRSS:) process_rss="$value${rest:+ $rest}" ;;
Threads:) process_threads="$value" ;;
esac
done < "/proc/$target_pid/status"
else
mark_warning
fi
root_fs_type="unknown"
root_mount_source="unknown"
while read -r mount_id parent_id major_minor mount_root mount_point mount_options remaining; do
if [[ "$mount_point" == "/" ]]; then
filesystem_fields="${remaining#*- }"
read -r root_fs_type root_mount_source root_super_options <<< "$filesystem_fields"
break
fi
done < /proc/self/mountinfo
if [[ "$root_fs_type" == "unknown" ]]; then
mark_warning
fi
printf 'Process %s: %s, state %s, RSS %s, threads %s\n' "$target_pid" "$process_name" "$process_state" "$process_rss" "$process_threads"
printf 'Root storage: source %s, filesystem %s\n' "$root_mount_source" "$root_fs_type"
Your monitor now explains which process it inspected plus what backs the root filesystem. Next, you will add network visibility and save each health snapshot for future automation.
Add Networking and Logging
Your Bash monitor can now inspect a target process. It can also identify the storage behind the root mount.
The report still lacks network visibility. Its output also disappears when the terminal session ends.
This step reads interface states from sysfs. A shared output function writes each line to the terminal plus a local log.
In this step, get ready to:
- Collect every network interface with its operational state.
- Send every report line through one logging function.
- Verify the saved report plus its automation-friendly exit statuses.
Add network interface states
Each directory under /sys/class/net/* represents a network interface. Its operstate file exposes the interface's current operational state.
- In linux-health.sh, find the block that checks whether root_fs_type is still unknown.
- Insert the networking loop below that block by pasting this code:
network_summary=""
network_count=0
for interface_path in /sys/class/net/*; do
[[ -d "$interface_path" ]] || continue
interface_name="${interface_path##*/}"
interface_state="unknown"
if [[ -r "$interface_path/operstate" ]]; then
read -r interface_state < "$interface_path/operstate"
fi
if [[ -n "$network_summary" ]]; then
network_summary="$network_summary, "
fi
network_summary="${network_summary}${interface_name}=${interface_state}"
(( network_count += 1 ))
done
if (( network_count == 0 )); then
network_summary="no interfaces found"
mark_warning
fi
What does this code do?
- The loop visits each directory under /sys/class/net.
- The interface_name expansion removes the directory prefix from each path.
- The interface_state value comes from the interface's readable operstate file.
- An empty interface list calls mark_warning because the snapshot has lost a required view of the host.
- Save linux-health.sh.
- Execute the edited script through Bash's syntax gate by running this command:
bash -n linux-health.sh && bash linux-health.sh
What does this check prove?
The -n option checks the script without executing its commands. Bash runs the script only when that syntax check succeeds.
You should see the existing process details plus root storage details. This confirms the new networking loop parses successfully on your Linux host.
Does the script stop during the networking loop?
- Check that the loop begins with for interface_path in /sys/class/net/*.
- Confirm that every if block has a matching fi.
- Confirm that the loop closes with done.
Help me debug the networking loop in my Bash health monitor.
Create one output path
A logging function keeps terminal output plus saved output consistent. Every report line passes through emit once.
- Add the log filename above the existing target_pid assignment by pasting this line:
log_file="linux-health.log"
What does this value control?
The log_file variable gives every snapshot the same local destination. Repeated runs can reuse that path.
- Find the closing brace of mark_warning.
- Add the emit function directly below it by pasting this code:
emit() {
local message="$1"
printf '%s\n' "$message"
printf '%s\n' "$message" >> "$log_file"
}
How does emit work?
- The local message variable holds one report line.
- The first printf sends that line to the terminal.
- The second printf appends the same line to linux-health.log.
- Append redirection creates the log file when it does not exist.
- Save linux-health.sh.
- Confirm the new function parses by running this command:
bash -n linux-health.sh && bash linux-health.sh
What should happen now?
You should see the process details plus root storage details again. The successful run proves Bash can load the new emit function.
Good progress. Your monitor now has one reusable route for terminal output plus saved output.
Does Bash reject the emit function?
- Confirm that emit() appears after the closing brace of mark_warning.
- Check that both printf lines remain inside the emit braces.
- Check that the second printf uses append redirection toward log_file.
Help me fix the emit function in my Bash script.
Complete and verify the report
The final report now presents every collected section through emit. The script finishes with the accumulated exit_code so another script can react to the result.
- Find the two final printf lines that display process details plus root storage details.
- Replace those two lines with the completed report block below:
emit "===== Linux Health Snapshot ====="
emit "Uptime: $uptime_seconds seconds"
emit "Load averages: $load_1 $load_5 $load_15"
emit "Tasks: $task_counts"
emit "Memory: ${available_percent}% available, ${used_percent}% used, threshold ${memory_warning_percent}% [$memory_status]"
if (( process_readable == 1 )); then
emit "Process $target_pid: $process_name, state $process_state, RSS $process_rss, threads $process_threads"
else
emit "Process $target_pid: missing or unreadable [WARNING]"
fi
emit "Root storage: source $root_mount_source, filesystem $root_fs_type"
emit "Network interfaces ($network_count): $network_summary"
emit "Overall status: $overall_status"
emit "Report file: $log_file"
emit ""
exit "$exit_code"
What does the completed report do?
- The opening lines report uptime plus the three load averages.
- The memory line includes available memory plus used memory.
- The process branch keeps missing process data visible as a warning.
- The final lines report root storage plus network state plus the overall result.
- Save linux-health.sh.
Before the final checks, the key question is which report lines should appear in both output locations.
- Check the complete script syntax before running the health snapshot with this command:
bash -n linux-health.sh && bash linux-health.sh
What should you see?
- You should see uptime plus three load averages plus task counts.
- You should see available memory plus used memory plus its warning threshold.
- You should see the target process plus root storage details.
- You should see interface names with their states followed by the overall status.
A healthy snapshot exits with status 0. A warning exits with status 1.
Invalid input or unavailable required kernel data exits with status 2. These values let future automation distinguish each outcome.
- Confirm that linux-health.log contains saved report data by running this command:
[[ -s linux-health.log ]] && printf '%s\n' 'Log saved'
What does the log check prove?
The -s test succeeds when linux-health.log exists with data inside it. You should see Log saved in the terminal.
Missing interface output or log data?
- Confirm that the networking loop appears before the first emit call.
- Check that every report line uses emit.
- Confirm that log_file is set before the emit function runs.
Help me trace why my network summary or log file is missing.
✔️ Awesome, I've got everything!
Excellent. Your monitor prints a complete snapshot while preserving the same report in linux-health.log.
ⓧ I'd like to double check the full code
- Compare your saved linux-health.sh file with this complete reference:
log_file="linux-health.log"
target_pid="${1:-$$}"
memory_warning_percent="${2:-20}"
usage() {
printf '%s\n' "Usage: bash linux-health.sh [pid] [memory-warning-percent]"
}
case "$target_pid" in
''|*[!0-9]*)
usage
exit 2
;;
esac
case "$memory_warning_percent" in
''|*[!0-9]*)
usage
exit 2
;;
esac
if (( memory_warning_percent < 1 || memory_warning_percent > 100 )); then
usage
exit 2
fi
for required_path in \
/proc/meminfo \
/proc/loadavg \
/proc/uptime \
/proc/self/mountinfo \
/sys/class/net
do
if [[ ! -r "$required_path" ]]; then
printf 'Required path is not readable: %s\n' "$required_path"
exit 2
fi
done
overall_status="OK"
exit_code=0
mark_warning() {
overall_status="WARNING"
exit_code=1
}
emit() {
local message="$1"
printf '%s\n' "$message"
printf '%s\n' "$message" >> "$log_file"
}
mem_total_kb=0
mem_available_kb=0
while read -r key value unit; do
case "$key" in
MemTotal:)
mem_total_kb="$value"
;;
MemAvailable:)
mem_available_kb="$value"
;;
esac
done < /proc/meminfo
if (( mem_total_kb == 0 )); then
printf '%s\n' "Could not read MemTotal from /proc/meminfo"
exit 2
fi
available_percent=$(( mem_available_kb * 100 / mem_total_kb ))
used_percent=$(( 100 - available_percent ))
memory_status="OK"
if (( available_percent < memory_warning_percent )); then
memory_status="WARNING"
mark_warning
fi
read -r load_1 load_5 load_15 task_counts last_pid < /proc/loadavg
read -r uptime_value idle_value < /proc/uptime
uptime_seconds="${uptime_value%%.*}"
process_readable=0
process_name="unknown"
process_state="unknown"
process_rss="unknown"
process_threads="unknown"
if [[ -r "/proc/$target_pid/status" ]]; then
process_readable=1
while read -r key value rest; do
case "$key" in
Name:)
process_name="$value${rest:+ $rest}"
;;
State:)
process_state="$value${rest:+ $rest}"
;;
VmRSS:)
process_rss="$value${rest:+ $rest}"
;;
Threads:)
process_threads="$value"
;;
esac
done < "/proc/$target_pid/status"
else
mark_warning
fi
root_fs_type="unknown"
root_mount_source="unknown"
while read -r mount_id parent_id major_minor mount_root mount_point mount_options remaining; do
if [[ "$mount_point" == "/" ]]; then
filesystem_fields="${remaining#*- }"
read -r root_fs_type root_mount_source root_super_options <<< "$filesystem_fields"
break
fi
done < /proc/self/mountinfo
if [[ "$root_fs_type" == "unknown" ]]; then
mark_warning
fi
network_summary=""
network_count=0
for interface_path in /sys/class/net/*; do
[[ -d "$interface_path" ]] || continue
interface_name="${interface_path##*/}"
interface_state="unknown"
if [[ -r "$interface_path/operstate" ]]; then
read -r interface_state < "$interface_path/operstate"
fi
if [[ -n "$network_summary" ]]; then
network_summary="$network_summary, "
fi
network_summary="${network_summary}${interface_name}=${interface_state}"
(( network_count += 1 ))
done
if (( network_count == 0 )); then
network_summary="no interfaces found"
mark_warning
fi
emit "===== Linux Health Snapshot ====="
emit "Uptime: $uptime_seconds seconds"
emit "Load averages: $load_1 $load_5 $load_15"
emit "Tasks: $task_counts"
emit "Memory: ${available_percent}% available, ${used_percent}% used, threshold ${memory_warning_percent}% [$memory_status]"
if (( process_readable == 1 )); then
emit "Process $target_pid: $process_name, state $process_state, RSS $process_rss, threads $process_threads"
else
emit "Process $target_pid: missing or unreadable [WARNING]"
fi
emit "Root storage: source $root_mount_source, filesystem $root_fs_type"
emit "Network interfaces ($network_count): $network_summary"
emit "Overall status: $overall_status"
emit "Report file: $log_file"
emit ""
exit "$exit_code"
How to use this reference
This reference shows the complete positional-argument monitor at the end of the step. Check the order of each section plus the spacing around complete functions and loops.
You completed the monitor's full reporting path. Each run now captures resource data plus network state in a reusable local report.
Secret mission
Add Named Command-Line Options
Positional arguments become harder to remember as your monitor grows. Replace them with named options for process selection, memory thresholds, interface filtering, and help.
Clean Up Your Resources
Clean Up Your Resources
Your Bash health monitor creates no ongoing project cost. Decide whether to keep both project files, pause your work, or delete the files entirely.
Resources you used:
- The linux-health.sh script in your current shell directory.
- The linux-health.log report in the same directory.
Keep everything running
Keep both files if you want to continue running health checks on your existing Linux VM or server.
- Keep linux-health.sh in the current directory.
- Keep linux-health.log as a history of your saved health snapshots.
The monitor writes new data only when you run it.
Pause - I'll come back to this later
The script exits after every snapshot. No background monitor process needs stopping.
- Leave linux-health.sh unchanged until your next session.
- Retain linux-health.log if you want to compare future snapshots with earlier reports.
Your files remain ready for the next time you connect to the server.
Delete - I don't want to use this again
Deleting these files is permanent. The command targets only the two project files in your current directory.
- Remove the script and its saved report by running this command:
rm -f linux-health.sh linux-health.log && printf '%s\n' 'Project files removed'
What Gets Deleted?
The command deletes only linux-health.sh and linux-health.log from the current directory. It leaves your existing Linux VM or server unchanged.
You'll see Project files removed when the cleanup succeeds.
Nice Work!
Nice Work!
You did it! You built a reusable Linux health monitor with Bash that turns kernel data into an automation-ready snapshot.
You've learned how to:
- Built a reusable health snapshot that reports Linux resource data. It also reports process details, root storage information, and network interface states.
- Corrected a misleading MemFree signal by switching to MemAvailable for useful memory headroom warnings.
- Added automation-ready behavior through validated input, append-only logging, and distinct exit statuses. Saved each snapshot to linux-health.log.
- Completed the Secret Mission by adding named command-line options with getopts. Added optional network-interface filtering.
Ready to quiz yourself?