Build a Server Log Monitor in Bash
Write a Bash script that parses server logs and auto-alerts on errors.
Introduction
30 Second Summary
Every application running in production generates logs. When something goes wrong at 3am, the logs are the first thing any engineer checks.
In this project, you will build a Bash script that automatically detects and counts errors from server log files, generates a summary report, and triggers an alert when errors exceed a threshold. You will then schedule it with cron to run automatically every 5 minutes.
What You'll Build
A production-style log monitoring system written entirely in Bash that parses application logs, counts errors by type, generates summary reports, and triggers threshold-based alerts.
By the end of this project, you'll have:
- A simulated production log file with realistic entries across multiple severity levels.
- A Bash monitoring script using grep, awk, pipes, functions, conditionals, and threshold-based alerting.
- A working cron job that runs the monitoring script on a schedule without human intervention.
- Secret Mission: Extend the system to monitor multiple log files and add color-coded terminal output by severity level.
Are there any prerequisites?
You need WSL with Ubuntu installed on Windows. If you haven't set that up yet, search for "Install WSL" in the Microsoft Store or run wsl --install from PowerShell.
Set Up Your Environment
Your log monitoring system needs a solid foundation before you write any code. Before jumping into scripting, you need to confirm that your Linux environment is healthy and that the tools you'll depend on are available.
In this step, you'll verify your Bash shell, get the cron scheduling service running, and create the folder structure that will organize your logs, scripts, and reports.
In this step, get ready to:
- Confirm your WSL Ubuntu environment and Bash shell are working.
- Install and start the cron scheduling service.
- Create the project directory structure with proper permissions.
Verify your environment is ready
WSL (Windows Subsystem for Linux) gives you a full Linux terminal on Windows. Your monitoring scripts will run in this environment, so let's confirm everything is working.
- Check your Bash version by running this command:
bash --version
You should see output showing GNU bash followed by a version number.
- Confirm you're running Ubuntu by running:
cat /etc/os-release
You should see details about your Ubuntu installation, including the version name and ID.
Don't see Ubuntu info?
Make sure you opened your WSL Ubuntu terminal, not PowerShell or Command Prompt. You can open it by searching for Ubuntu in the Windows search bar.
help me verify my WSL Ubuntu is working
Install and start the cron service
Cron is a scheduling daemon that runs tasks automatically at specified intervals. You'll use it later to run your monitoring script every 5 minutes without any manual intervention.
- Install cron (or confirm it's already installed) by running these commands:
sudo apt update && sudo apt install -y cron
What does this command do?
sudo apt update refreshes the list of available packages so your system knows what's out there.
sudo apt install -y cron installs the cron package. The -y flag automatically answers "yes" to the installation prompt. If cron is already installed, this command safely confirms it's up to date.
- Start the cron service by running:
sudo service cron start
- Verify cron is running by checking its status:
sudo service cron status
✔️ I see cron is running
You should see output indicating that cron is active and running, along with a process ID number. This confirms the scheduler is ready to accept jobs.
ⓧ I see it's not running or an error
If cron didn't start, try stopping and restarting it:
sudo service cron stop
sudo service cron start
sudo service cron status
If you still see an error, your WSL instance may need systemd enabled. Check that your /etc/wsl.conf file contains the following:
[boot]
systemd=true
- After editing the file, close your WSL terminal and restart it by opening Ubuntu from the Windows search bar again.
- Run sudo service cron start followed by sudo service cron status again to confirm.
help me get cron running in WSL Ubuntu
Why does cron need to be started manually?
In WSL, background services like cron don't automatically start when you open your terminal. You'll need to run sudo service cron start each time you open a new WSL session (unless systemd is enabled and configured to persist services).
Create the project directory structure
A well-organized directory structure keeps your logs, scripts, and reports separate. This mirrors how production systems organize monitoring tools.
- Create the project directory with all subdirectories by running this command:
mkdir -p ~/log-monitor/{logs,scripts,reports}
What does this command do?
mkdir -p creates directories and any parent directories that don't exist yet.
The {logs,scripts,reports} syntax is called brace expansion. Bash expands it into three separate paths: ~/log-monitor/logs, ~/log-monitor/scripts, and ~/log-monitor/reports.
- Set appropriate permissions on the scripts directory by running:
chmod 755 ~/log-monitor/scripts
What does chmod 755 mean?
chmod changes file or directory permissions. The number 755 means the owner can read, write, and execute, while others can only read and execute.
This is important for the scripts directory because your monitoring scripts need to be executable. Setting this now means you won't hit permission errors later.
- Verify the directory structure was created correctly by running:
ls -la ~/log-monitor
You should see three subdirectories listed: logs, scripts, and reports.
Don't see the directories?
Make sure you typed the brace expansion {logs,scripts,reports} without any spaces after the commas. Spaces inside braces will cause the expansion to fail silently.
help me create my log-monitor directory structure
Your environment is locked and loaded. Next up, you'll create realistic simulated server logs that your monitoring script will analyze.
Create Simulated Production Logs
Your environment is set up and ready to go. Now you need something to monitor.
Real production servers generate thousands of log entries every hour. In this step, you will write a Bash script that simulates that output by generating 200 randomized log entries with realistic timestamps, severity levels, and service names. Once the log file exists, you will practice the same text-processing commands that your monitoring script will automate later.
In this step, get ready to:
- Write a Bash script that generates a realistic application log file.
- Run the generator and inspect the output with standard Linux commands.
- Practice grep and pipe commands to count errors manually.
Write the log generator script
The generator script uses Bash arrays to store possible log levels, service names, and messages. A loop picks random combinations and writes each entry with a timestamp to a log file.
- Create the script file by running:
nano ~/log-monitor/scripts/generate-logs.sh
- Paste the following script into nano:
#!/usr/bin/env bash
set -euo pipefail
# Generate simulated production logs
LOG_FILE="$HOME/log-monitor/logs/app.log"
# Clear existing log
> "$LOG_FILE"
# Arrays of log components
LEVELS=("INFO" "INFO" "INFO" "WARNING" "WARNING" "ERROR" "ERROR" "CRITICAL")
SERVICES=("auth-service" "api-gateway" "database" "cache" "worker")
MESSAGES_INFO=("Request processed successfully" "Connection established" "Cache hit" "Health check passed" "Session started")
MESSAGES_WARNING=("High memory usage detected" "Slow query detected" "Rate limit approaching" "Disk usage above 80%" "Connection pool near capacity")
MESSAGES_ERROR=("Connection timeout" "Database query failed" "Authentication failed" "Service unavailable" "File not found")
MESSAGES_CRITICAL=("Out of memory" "Disk full" "Database connection lost" "Unhandled exception" "System overload")
# Generate 200 log entries
for i in $(seq 1 200); do
# Random timestamp within the last hour
MINUTES_AGO=$((RANDOM % 60))
TIMESTAMP=$(date -d "-${MINUTES_AGO} minutes" "+%Y-%m-%d %H:%M:%S")
# Random level
LEVEL="${LEVELS[$((RANDOM % ${#LEVELS[@]}))]}"
# Random service
SERVICE="${SERVICES[$((RANDOM % ${#SERVICES[@]}))]}"
# Message based on level
case "$LEVEL" in
"INFO")
MSG="${MESSAGES_INFO[$((RANDOM % ${#MESSAGES_INFO[@]}))]}"
;;
"WARNING")
MSG="${MESSAGES_WARNING[$((RANDOM % ${#MESSAGES_WARNING[@]}))]}"
;;
"ERROR")
MSG="${MESSAGES_ERROR[$((RANDOM % ${#MESSAGES_ERROR[@]}))]}"
;;
"CRITICAL")
MSG="${MESSAGES_CRITICAL[$((RANDOM % ${#MESSAGES_CRITICAL[@]}))]}"
;;
esac
echo "[$TIMESTAMP] $LEVEL $SERVICE: $MSG" >> "$LOG_FILE"
done
# Sort by timestamp
sort -o "$LOG_FILE" "$LOG_FILE"
echo "Generated 200 log entries in $LOG_FILE"
What does this script do?
The script has three main sections that work together to produce a realistic log file.
- set -euo pipefail activates safety flags. The script exits on any error (-e), treats unset variables as errors (-u), and catches failures in piped commands (pipefail).
- The LEVELS array contains duplicates like three INFOs and two WARNINGs. This biases the randomness so INFO appears more often than CRITICAL, just like a real server.
- The for loop runs 200 times, each iteration picking a random level, service, and matching message. A case statement selects the right message array based on the chosen level.
- sort -o at the end reorders entries by timestamp so the log reads chronologically, just like a real application log.
- Save and exit nano by pressing Ctrl+O, then Enter to confirm the filename, then Ctrl+X to exit.
- Make the script executable by running:
chmod +x ~/log-monitor/scripts/generate-logs.sh
- Confirm the file exists and is executable by running:
ls -la ~/log-monitor/scripts/generate-logs.sh
You should see -rwxr-xr-x at the start of the output, confirming execute permissions are set.
Seeing "Permission denied" or the file is missing?
- Make sure you saved in nano before exiting. If the file is missing, open nano again with the same command and re-paste the script.
- If you see -rw-r--r-- instead of -rwxr-xr-x, run the chmod +x command again.
- Check for typos in the path. The file should be at ~/log-monitor/scripts/generate-logs.sh.
Can't figure out what went wrong? Ask for help.
✔️ Awesome, I've got everything!
Great. Double check you saved the file before moving on.
ⓧ I'd like to double check the full code
#!/usr/bin/env bash
set -euo pipefail
# Generate simulated production logs
LOG_FILE="$HOME/log-monitor/logs/app.log"
# Clear existing log
> "$LOG_FILE"
# Arrays of log components
LEVELS=("INFO" "INFO" "INFO" "WARNING" "WARNING" "ERROR" "ERROR" "CRITICAL")
SERVICES=("auth-service" "api-gateway" "database" "cache" "worker")
MESSAGES_INFO=("Request processed successfully" "Connection established" "Cache hit" "Health check passed" "Session started")
MESSAGES_WARNING=("High memory usage detected" "Slow query detected" "Rate limit approaching" "Disk usage above 80%" "Connection pool near capacity")
MESSAGES_ERROR=("Connection timeout" "Database query failed" "Authentication failed" "Service unavailable" "File not found")
MESSAGES_CRITICAL=("Out of memory" "Disk full" "Database connection lost" "Unhandled exception" "System overload")
# Generate 200 log entries
for i in $(seq 1 200); do
# Random timestamp within the last hour
MINUTES_AGO=$((RANDOM % 60))
TIMESTAMP=$(date -d "-${MINUTES_AGO} minutes" "+%Y-%m-%d %H:%M:%S")
# Random level
LEVEL="${LEVELS[$((RANDOM % ${#LEVELS[@]}))]}"
# Random service
SERVICE="${SERVICES[$((RANDOM % ${#SERVICES[@]}))]}"
# Message based on level
case "$LEVEL" in
"INFO")
MSG="${MESSAGES_INFO[$((RANDOM % ${#MESSAGES_INFO[@]}))]}"
;;
"WARNING")
MSG="${MESSAGES_WARNING[$((RANDOM % ${#MESSAGES_WARNING[@]}))]}"
;;
"ERROR")
MSG="${MESSAGES_ERROR[$((RANDOM % ${#MESSAGES_ERROR[@]}))]}"
;;
"CRITICAL")
MSG="${MESSAGES_CRITICAL[$((RANDOM % ${#MESSAGES_CRITICAL[@]}))]}"
;;
esac
echo "[$TIMESTAMP] $LEVEL $SERVICE: $MSG" >> "$LOG_FILE"
done
# Sort by timestamp
sort -o "$LOG_FILE" "$LOG_FILE"
echo "Generated 200 log entries in $LOG_FILE"
Run the log generator and inspect the output
With the script ready, it is time to generate the log file and verify it looks like real server output.
- Run the generator script by executing:
bash ~/log-monitor/scripts/generate-logs.sh
You should see the message Generated 200 log entries in /home/<your-username>/log-monitor/logs/app.log printed to your terminal.
- View the first 10 lines of the log file by running:
head ~/log-monitor/logs/app.log
- View the last 10 lines of the log file by running:
tail ~/log-monitor/logs/app.log
- Count the total number of lines in the file by running:
wc -l ~/log-monitor/logs/app.log
You should see 200 as the line count, confirming all entries were generated.
What do these commands do?
- head shows the first 10 lines of a file. Useful for checking format without flooding your terminal.
- tail shows the last 10 lines. In production, you use this to see the most recent log entries.
- wc -l counts lines. It is your quick sanity check that the file has the expected size.
Seeing 0 lines or "No such file"?
- If you see "No such file or directory", confirm the logs/ directory exists with ls ~/log-monitor/logs/. If it is missing, create it with mkdir -p ~/log-monitor/logs and run the script again.
- If the file exists but has 0 lines, check that your script's LOG_FILE variable points to $HOME/log-monitor/logs/app.log. Open it in nano and compare against the full code above.
Still stuck? Ask for help.
Count errors with grep
The monitoring script you will build next automates error counting. First, try it manually so you understand exactly what the automation replaces.
- Count how many ERROR lines appear in the log by running:
grep "ERROR" ~/log-monitor/logs/app.log | wc -l
What is happening in this command?
This command uses a pipe to chain two programs together.
- grep "ERROR" filters the log file and outputs only lines containing the word ERROR.
- The pipe (|) sends that filtered output directly into the next command.
- wc -l counts how many lines it received, giving you the total error count.
This grep-pipe-count pattern is one of the most common tools in a DevOps engineer's daily toolkit.
- Try counting CRITICAL entries separately by running:
grep "CRITICAL" ~/log-monitor/logs/app.log | wc -l
You should see fewer CRITICAL entries than ERROR entries, since the LEVELS array only includes one CRITICAL compared to two ERRORs.
- Now try combining both error types into one count by running:
grep -c "ERROR\|CRITICAL" ~/log-monitor/logs/app.log
Why use grep -c instead of wc -l?
The -c flag tells grep to count matching lines internally, so you do not need a separate wc -l command. The \| syntax means "or" in grep, matching lines with either ERROR or CRITICAL.
Your monitoring script will use this exact pattern to count all problems at once.
Getting a count of 0?
- The word inside the quotes is case-sensitive. Make sure you wrote ERROR in all caps, not error or Error.
- Verify your log file has content by running wc -l ~/log-monitor/logs/app.log. If it shows 0, re-run the generator script.
Still seeing unexpected results? Ask for help.
You now have a realistic log file and the command-line skills to search through it. Next up, you will build the monitoring script that automates everything you just did by hand.
Build the Core Log Analysis Script
You have a realistic log file full of mixed entries and you know how to manually search through it with grep.
Now it is time to turn those manual commands into an automated script. In this step, you will write monitor.sh which reads your log file, counts errors, and writes a clean summary report.
In this step, get ready to:
- Create a monitoring script with a proper shebang, safety flags, and configuration variables.
- Add text processing logic using pipes, grep, awk, sort, and uniq.
- Run the script and verify it produces a correct report file.
Create the monitoring script with configuration
Every production Bash script starts with a shebang line (telling the system which interpreter to use), safety flags (catching bugs early), and configuration variables (making the script easy to adjust later).
- Open a new file in nano by running this command:
nano ~/log-monitor/scripts/monitor.sh
- Type or paste the following code into the editor:
#!/usr/bin/env bash
set -euo pipefail
# Configuration
LOG_FILE="$HOME/log-monitor/logs/app.log"
REPORT_FILE="$HOME/log-monitor/reports/report.txt"
ERROR_THRESHOLD=10
# Verify log file exists
if [ ! -f "$LOG_FILE" ]; then
echo "ERROR: Log file not found: $LOG_FILE" >&2
exit 1
fi
What does this code do?
This first section sets up the script's foundation before any real processing happens.
- #!/usr/bin/env bash is the shebang line. It tells the system to run this file with Bash, using the portable env lookup so it works across different Linux environments.
- set -euo pipefail enables three safety flags. -e exits on any error. -u catches typos in variable names. -o pipefail catches failures inside pipes.
- The three variables (LOG_FILE, REPORT_FILE, ERROR_THRESHOLD) keep all paths and settings in one place so you can adjust them without hunting through the script.
- The if [ ! -f ... ] block checks that the log file actually exists before the script tries to process it. If the file is missing, it prints an error to stderr and exits cleanly.
- Save and exit nano by pressing Ctrl+O, then Enter, then Ctrl+X.
- Confirm the file was created by running:
cat ~/log-monitor/scripts/monitor.sh
You should see the shebang, safety flags, configuration variables, and the file-existence check printed to the terminal.
Not seeing your code?
- Make sure you pressed Ctrl+O (the letter O, not zero) to save before exiting.
- Check that the path matches exactly. Run ls ~/log-monitor/scripts/ to verify monitor.sh is listed.
- help me troubleshoot my nano save issue
Add text processing logic to generate the report
The real power of Bash scripting comes from chaining small tools together with pipes. Each tool does one thing well, and the pipeline produces a complete analysis.
- Reopen monitor.sh in nano by running:
nano ~/log-monitor/scripts/monitor.sh
- Place your cursor at the very end of the file (after the fi line).
- Add a blank line, then type or paste the following code below the existing content:
# Count errors
ERROR_COUNT=$(grep -c "ERROR\|CRITICAL" "$LOG_FILE" || echo "0")
# Generate report
echo "=============================" > "$REPORT_FILE"
echo " LOG MONITOR REPORT" >> "$REPORT_FILE"
echo " $(date '+%Y-%m-%d %H:%M:%S')" >> "$REPORT_FILE"
echo "=============================" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
echo "Log file: $LOG_FILE" >> "$REPORT_FILE"
echo "Total lines: $(wc -l < "$LOG_FILE")" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
echo "--- Severity Summary ---" >> "$REPORT_FILE"
echo "INFO: $(grep -c "INFO" "$LOG_FILE" || echo "0")" >> "$REPORT_FILE"
echo "WARNING: $(grep -c "WARNING" "$LOG_FILE" || echo "0")" >> "$REPORT_FILE"
echo "ERROR: $(grep -c "ERROR" "$LOG_FILE" || echo "0")" >> "$REPORT_FILE"
echo "CRITICAL: $(grep -c "CRITICAL" "$LOG_FILE" || echo "0")" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
echo "--- Errors by Service ---" >> "$REPORT_FILE"
grep "ERROR\|CRITICAL" "$LOG_FILE" | awk '{print $4}' | sort | uniq -c | sort -rn >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
echo "--- Error Types ---" >> "$REPORT_FILE"
grep "ERROR\|CRITICAL" "$LOG_FILE" | awk -F': ' '{print $2}' | sort | uniq -c | sort -rn >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
echo "Report generated: $REPORT_FILE"
What does this code do?
This section uses pipes and redirection to analyze the log and write a formatted report.
- grep -c "ERROR\|CRITICAL" counts lines matching either ERROR or CRITICAL. The || echo "0" prevents the script from crashing when grep finds zero matches (since grep returns exit code 1 on no match, which would trigger set -e).
- > overwrites the report file fresh each run, while >> appends each subsequent line.
- The grep ... | awk '{print $4}' | sort | uniq -c | sort -rn pipeline filters error lines, extracts the service name (4th field), sorts them, counts duplicates, and sorts by frequency.
- The awk -F': ' '{print $2}' pipeline uses a colon-space delimiter to extract the error message text after the service name.
Why the || echo "0" pattern?
When grep -c finds no matches, it returns exit code 1. Normally that is fine, but set -e treats any non-zero exit as a fatal error.
The || echo "0" tells Bash: if grep fails, just output 0 instead. This handles the edge case gracefully without disabling safety flags for the whole script.
- Save and exit nano by pressing Ctrl+O, then Enter, then Ctrl+X.
Worried about typos in the long code block?
- Pay close attention to the difference between > (first line, overwrites) and >> (all other lines, appends). Using > on the wrong line will erase everything above it in the report.
- Make sure quotes match. Every opening " needs a closing " on the same line.
- help me check my monitor.sh for syntax errors
Make the script executable and run it
Linux does not let you run a script file unless you explicitly mark it as executable. The chmod +x command grants that permission.
- Make the script executable by running:
chmod +x ~/log-monitor/scripts/monitor.sh
- Run the monitoring script by executing:
~/log-monitor/scripts/monitor.sh
You should see a message that says Report generated: /home/<your-username>/log-monitor/reports/report.txt.
- Inspect the generated report by running:
cat ~/log-monitor/reports/report.txt
You should see a formatted report with a dated header, total line count, a severity summary section showing counts for INFO, WARNING, ERROR, and CRITICAL, followed by breakdowns of errors by service and error type.
Seeing an error when you run the script?
- If you see Permission denied, the chmod +x step did not work. Run it again and make sure the path is correct.
- If you see ERROR: Log file not found, that means app.log does not exist. Go back to your previous step and run generate-logs.sh again.
- If you see a syntax error with a line number, reopen the script in nano and check that line for missing quotes or typos.
- help me debug my monitor.sh script error
✔️ Awesome, I've got everything!
Great. Double check that you have saved monitor.sh and that report.txt exists in your reports/ directory.
ⓧ I'd like to double check the full code
Here is the complete monitor.sh file. Make sure yours matches exactly:
#!/usr/bin/env bash
set -euo pipefail
# Configuration
LOG_FILE="$HOME/log-monitor/logs/app.log"
REPORT_FILE="$HOME/log-monitor/reports/report.txt"
ERROR_THRESHOLD=10
# Verify log file exists
if [ ! -f "$LOG_FILE" ]; then
echo "ERROR: Log file not found: $LOG_FILE" >&2
exit 1
fi
# Count errors
ERROR_COUNT=$(grep -c "ERROR\|CRITICAL" "$LOG_FILE" || echo "0")
# Generate report
echo "=============================" > "$REPORT_FILE"
echo " LOG MONITOR REPORT" >> "$REPORT_FILE"
echo " $(date '+%Y-%m-%d %H:%M:%S')" >> "$REPORT_FILE"
echo "=============================" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
echo "Log file: $LOG_FILE" >> "$REPORT_FILE"
echo "Total lines: $(wc -l < "$LOG_FILE")" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
echo "--- Severity Summary ---" >> "$REPORT_FILE"
echo "INFO: $(grep -c "INFO" "$LOG_FILE" || echo "0")" >> "$REPORT_FILE"
echo "WARNING: $(grep -c "WARNING" "$LOG_FILE" || echo "0")" >> "$REPORT_FILE"
echo "ERROR: $(grep -c "ERROR" "$LOG_FILE" || echo "0")" >> "$REPORT_FILE"
echo "CRITICAL: $(grep -c "CRITICAL" "$LOG_FILE" || echo "0")" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
echo "--- Errors by Service ---" >> "$REPORT_FILE"
grep "ERROR\|CRITICAL" "$LOG_FILE" | awk '{print $4}' | sort | uniq -c | sort -rn >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
echo "--- Error Types ---" >> "$REPORT_FILE"
grep "ERROR\|CRITICAL" "$LOG_FILE" | awk -F': ' '{print $2}' | sort | uniq -c | sort -rn >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
echo "Report generated: $REPORT_FILE"
Your monitoring script works. Next up, you will refactor it with functions and add threshold-based alerting so it can warn you when errors spike.
Add Functions, Alerting, and Error Handling
Your monitoring script already reads the log file, counts errors, and generates a structured report. Right now though, all of that logic lives in one long sequence of commands with no separation between concerns.
In this step, you'll refactor the script into reusable functions and add automatic alerting. If errors exceed a configurable threshold, the script will sound the alarm by writing an alert to the report and printing a warning to the terminal.
In this step, get ready to:
- Refactor inline code into three modular functions.
- Add threshold-based alerting that writes to the report and stderr.
- Test the script with different thresholds to trigger both alert and all-clear paths.
Refactor the script into reusable functions
Production scripts wrap repeated logic inside functions so each piece can be tested, reused, and read independently. You'll restructure monitor.sh into three functions: count_errors() for counting, generate_report() for the summary, and check_threshold() for alert logic.
- Clear the existing content of monitor.sh by running this command:
> ~/log-monitor/scripts/monitor.sh
- Open the now-empty file in nano:
nano ~/log-monitor/scripts/monitor.sh
- Add the script header, configuration variables, and the first two functions by typing the following code:
#!/usr/bin/env bash
set -euo pipefail
# =============================================================
# Server Log Monitor and Auto-Alert System
# Monitors application logs, generates reports, alerts on errors
# =============================================================
# Configuration
LOG_FILE="$HOME/log-monitor/logs/app.log"
REPORT_FILE="$HOME/log-monitor/reports/report.txt"
ERROR_THRESHOLD=10
# -------------------------------------------------------------
# Functions
# -------------------------------------------------------------
count_errors() {
local log="$1"
grep -c "ERROR\|CRITICAL" "$log" || echo "0"
}
generate_report() {
local log="$1"
local report="$2"
echo "=============================" > "$report"
echo " LOG MONITOR REPORT" >> "$report"
echo " $(date '+%Y-%m-%d %H:%M:%S')" >> "$report"
echo "=============================" >> "$report"
echo "" >> "$report"
echo "Log file: $log" >> "$report"
echo "Total lines: $(wc -l < "$log")" >> "$report"
echo "" >> "$report"
# Count by severity
echo "--- Severity Summary ---" >> "$report"
echo "INFO: $(grep -c "INFO" "$log" || echo "0")" >> "$report"
echo "WARNING: $(grep -c "WARNING" "$log" || echo "0")" >> "$report"
echo "ERROR: $(grep -c "ERROR" "$log" || echo "0")" >> "$report"
echo "CRITICAL: $(grep -c "CRITICAL" "$log" || echo "0")" >> "$report"
echo "" >> "$report"
# Error breakdown by service
echo "--- Errors by Service ---" >> "$report"
grep "ERROR\|CRITICAL" "$log" | awk '{print $4}' | sort | uniq -c | sort -rn >> "$report"
echo "" >> "$report"
# Error breakdown by message
echo "--- Error Types ---" >> "$report"
grep "ERROR\|CRITICAL" "$log" | awk -F': ' '{print $2}' | sort | uniq -c | sort -rn >> "$report"
echo "" >> "$report"
}
What does this code do?
The script header uses the same shebang and safety flags as before. The configuration block sets up paths and the ERROR_THRESHOLD variable that controls when an alert fires.
- count_errors() takes a log file path as its argument and returns the combined count of ERROR and CRITICAL lines. The local keyword keeps the variable scoped inside the function so it doesn't leak into the rest of the script.
- generate_report() accepts both the log path and the report path as arguments, then writes the full report. This is the same logic you had before, but now it's reusable and self-contained.
You won't see a change yet because the file is incomplete. The next chunk adds the alerting function.
Add conditional alert logic with check_threshold()
A monitoring script that only reports is useful. A monitoring script that automatically alerts when something is wrong is production-ready. The check_threshold() function compares the error count against your threshold and writes either an ALERT or an all-clear status to the report.
- Directly below the generate_report() function you just typed, add the check_threshold() function:
check_threshold() {
local error_count="$1"
local threshold="$2"
local report="$3"
if [ "$error_count" -ge "$threshold" ]; then
echo "*** ALERT: Error count ($error_count) exceeds threshold ($threshold) ***" >> "$report"
echo "ALERT: High error rate detected ($error_count errors)" >&2
return 1
else
echo "Status: All clear. Error count ($error_count) is below threshold ($threshold)." >> "$report"
return 0
fi
}
What does this code do?
- if [ "$error_count" -ge "$threshold" ] uses the -ge operator (greater than or equal) to compare the error count against your threshold.
- When the threshold is exceeded, the function writes an ALERT line to the report AND echoes a warning to stderr using >&2. Writing to stderr means alert messages appear in the terminal even if stdout is redirected to a file.
- return 1 signals a non-zero exit for the alert path, while return 0 signals success for the all-clear path.
The file still isn't complete. The next chunk adds the Main section that calls these functions.
Wire up the main section and test both alert paths
Functions don't run until you call them. The Main section ties everything together by calling each function in order and handling the non-zero exit that check_threshold() returns when an alert fires.
- Below the check_threshold() function, add the Main section to complete the script:
# -------------------------------------------------------------
# Main
# -------------------------------------------------------------
# Verify log file exists
if [ ! -f "$LOG_FILE" ]; then
echo "ERROR: Log file not found: $LOG_FILE" >&2
exit 1
fi
# Count errors
ERROR_COUNT=$(count_errors "$LOG_FILE")
# Generate the report
generate_report "$LOG_FILE" "$REPORT_FILE"
# Check threshold (allow non-zero exit for alert)
set +e
check_threshold "$ERROR_COUNT" "$ERROR_THRESHOLD" "$REPORT_FILE"
ALERT_STATUS=$?
set -e
# Final status line
echo "-----------------------------" >> "$REPORT_FILE"
echo "Monitor completed at $(date '+%H:%M:%S')" >> "$REPORT_FILE"
echo "Report generated: $REPORT_FILE"
exit 0
What does this code do?
- The file existence check runs first. If the log file is missing, the script exits immediately with a helpful error message.
- set +e temporarily disables the exit-on-error behavior from set -e. This is necessary because check_threshold() intentionally returns 1 on alert, which would otherwise kill the script.
- ALERT_STATUS=$? captures the exit code so you can use it later if needed. Then set -e re-enables strict error handling.
- The final lines append a completion timestamp to the report, then print the report path to stdout.
- Save the file by pressing Ctrl+O, then Enter to confirm.
- Exit nano by pressing Ctrl+X.
- Run the refactored script:
./scripts/monitor.sh
You should see Report generated: followed by the full path to your report file printed to the terminal.
- Inspect the report to check for the alert or all-clear status:
cat ~/log-monitor/reports/report.txt
Because your generated log likely has more than 10 errors, you should see an ALERT line and the completion timestamp at the bottom of the report.
Don't see the alert line or getting a script error?
- If you see a syntax error, double check that every if statement has a matching fi and every function has matching curly braces.
- If you see "permission denied", the file may have lost its execute bit. Run chmod +x ~/log-monitor/scripts/monitor.sh to restore it.
- If the report shows "All clear" instead of an alert, your log file may have fewer than 10 errors. That's fine. You'll test both paths next.
Help me debug my monitor.sh script
Now test both alert paths to confirm the threshold logic works correctly.
- Test the alert path by temporarily setting a very low threshold. Open the script:
nano ~/log-monitor/scripts/monitor.sh
- Find this line near the top of the file:
ERROR_THRESHOLD=10
- Change 10 to 1 so the line reads:
ERROR_THRESHOLD=1
- Save with Ctrl+O, Enter, then exit with Ctrl+X.
- Run the script again:
./scripts/monitor.sh
You should see ALERT: High error rate detected printed to the terminal as a stderr warning.
- Now test the all-clear path by setting the threshold very high. Open the script again:
nano ~/log-monitor/scripts/monitor.sh
- Change ERROR_THRESHOLD=1 to ERROR_THRESHOLD=999.
- Save with Ctrl+O, Enter, then exit with Ctrl+X.
- Run the script:
./scripts/monitor.sh
- Check the report for the all-clear message:
tail -5 ~/log-monitor/reports/report.txt
You should see Status: All clear. followed by the completion timestamp. No alert this time.
- Set the threshold back to its production value. Open the script one more time:
nano ~/log-monitor/scripts/monitor.sh
- Change ERROR_THRESHOLD=999 back to ERROR_THRESHOLD=10.
- Save with Ctrl+O, Enter, then exit with Ctrl+X.
Why test both paths?
A conditional that only works in one direction is a bug waiting to happen. By verifying both the alert and the all-clear branches with different threshold values, you've confirmed the logic is correct before automating it with cron in the next step.
✔️ Awesome, I've got everything!
Great. Make sure you've saved monitor.sh with the threshold back at 10.
ⓧ I'd like to double check the full code
Here is the complete monitor.sh as it should look now:
#!/usr/bin/env bash
set -euo pipefail
# =============================================================
# Server Log Monitor and Auto-Alert System
# Monitors application logs, generates reports, alerts on errors
# =============================================================
# Configuration
LOG_FILE="$HOME/log-monitor/logs/app.log"
REPORT_FILE="$HOME/log-monitor/reports/report.txt"
ERROR_THRESHOLD=10
# -------------------------------------------------------------
# Functions
# -------------------------------------------------------------
count_errors() {
local log="$1"
grep -c "ERROR\|CRITICAL" "$log" || echo "0"
}
generate_report() {
local log="$1"
local report="$2"
echo "=============================" > "$report"
echo " LOG MONITOR REPORT" >> "$report"
echo " $(date '+%Y-%m-%d %H:%M:%S')" >> "$report"
echo "=============================" >> "$report"
echo "" >> "$report"
echo "Log file: $log" >> "$report"
echo "Total lines: $(wc -l < "$log")" >> "$report"
echo "" >> "$report"
# Count by severity
echo "--- Severity Summary ---" >> "$report"
echo "INFO: $(grep -c "INFO" "$log" || echo "0")" >> "$report"
echo "WARNING: $(grep -c "WARNING" "$log" || echo "0")" >> "$report"
echo "ERROR: $(grep -c "ERROR" "$log" || echo "0")" >> "$report"
echo "CRITICAL: $(grep -c "CRITICAL" "$log" || echo "0")" >> "$report"
echo "" >> "$report"
# Error breakdown by service
echo "--- Errors by Service ---" >> "$report"
grep "ERROR\|CRITICAL" "$log" | awk '{print $4}' | sort | uniq -c | sort -rn >> "$report"
echo "" >> "$report"
# Error breakdown by message
echo "--- Error Types ---" >> "$report"
grep "ERROR\|CRITICAL" "$log" | awk -F': ' '{print $2}' | sort | uniq -c | sort -rn >> "$report"
echo "" >> "$report"
}
check_threshold() {
local error_count="$1"
local threshold="$2"
local report="$3"
if [ "$error_count" -ge "$threshold" ]; then
echo "*** ALERT: Error count ($error_count) exceeds threshold ($threshold) ***" >> "$report"
echo "ALERT: High error rate detected ($error_count errors)" >&2
return 1
else
echo "Status: All clear. Error count ($error_count) is below threshold ($threshold)." >> "$report"
return 0
fi
}
# -------------------------------------------------------------
# Main
# -------------------------------------------------------------
# Verify log file exists
if [ ! -f "$LOG_FILE" ]; then
echo "ERROR: Log file not found: $LOG_FILE" >&2
exit 1
fi
# Count errors
ERROR_COUNT=$(count_errors "$LOG_FILE")
# Generate the report
generate_report "$LOG_FILE" "$REPORT_FILE"
# Check threshold (allow non-zero exit for alert)
set +e
check_threshold "$ERROR_COUNT" "$ERROR_THRESHOLD" "$REPORT_FILE"
ALERT_STATUS=$?
set -e
# Final status line
echo "-----------------------------" >> "$REPORT_FILE"
echo "Monitor completed at $(date '+%H:%M:%S')" >> "$REPORT_FILE"
echo "Report generated: $REPORT_FILE"
exit 0
Your script now has clean separation of concerns and alerts you automatically when errors spike. Next up, you'll schedule it with cron so it monitors your logs every 5 minutes without any human intervention.
Automate with Cron
Your monitoring script is complete. It counts errors, generates reports, and fires alerts when thresholds are exceeded. But right now, you have to remember to run it yourself every single time.
The whole point of a monitoring system is that it runs without you. Cron is the Linux task scheduler that executes commands on a repeating schedule. In this step, you'll register your script with cron so it runs automatically every 5 minutes.
In this step, get ready to:
- Register your monitoring script with cron on a 5-minute schedule.
- Verify the cron job fires and updates the report file automatically.
- Add output logging so you can debug cron execution issues.
Register your script with cron
Cron uses a minimal environment when it runs your commands. It does not load your shell profile, so environment variables like $HOME are not guaranteed to expand. You must use full absolute paths in every cron entry.
- First, confirm your WSL username by running:
whoami
Note down the output. You will use this username in the cron entry path.
- Verify the cron service is still running from your earlier setup:
sudo service cron status
Seeing cron is not running?
If cron stopped since the last step, restart it by running:
sudo service cron start
WSL does not automatically start background services between sessions. You need to start cron manually each time you open a new WSL window.
- Open your crontab for editing by running:
crontab -e
First time running crontab -e?
If this is your first time opening the crontab, Ubuntu will prompt you to select a text editor from a numbered list. Type the number for nano and press Enter.
- In the editor, scroll to the bottom of the file (past any comment lines starting with #) and add the following line:
*/5 * * * * /home/[[USERNAME="your WSL username"]]/log-monitor/scripts/monitor.sh
What does this cron schedule mean?
A cron schedule has five time fields followed by the command to run. The five fields are: minute, hour, day of month, month, and day of week.
The */5 in the first field means "every 5 minutes." The asterisks in the remaining four fields mean "every hour, every day, every month, every weekday." Together this runs your script once every 5 minutes, around the clock.
- Save and close the file by pressing Ctrl+O, then Enter, then Ctrl+X.
You should see a confirmation message like "crontab: installing new crontab" in your terminal.
Seeing errors when saving?
Make sure your cron entry is on a single line with no line breaks in the middle. The path must start with /home/ followed by your actual username, not the literal text <username>.
Help me fix my crontab entry
Verify the cron job is registered
Saving the crontab does not guarantee the entry is correct. You should always verify what cron actually has scheduled.
- List your registered cron jobs by running:
crontab -l
You should see your */5 * * * * entry with the full path to monitor.sh in the output.
Speed up testing with a 1-minute schedule
Waiting 5 minutes to confirm the job fires is slow. You can temporarily change */5 to */1 in your crontab (run crontab -e again) so it fires every minute. Just remember to change it back to */5 once you've confirmed it works.
- After the cron interval passes (1 minute if you used the tip above, or 5 minutes otherwise), check the report file's modification timestamp by running:
ls -la ~/log-monitor/reports/report.txt
The modification time should be recent, matching when cron last ran the script. This confirms your script is executing on schedule without any manual intervention.
Report timestamp not updating?
Confirm the cron service is running with sudo service cron status. If it stopped, restart it.
Check that your crontab path matches the actual location of monitor.sh. Run which bash and ls ~/log-monitor/scripts/monitor.sh to confirm both exist.
Help me debug why my cron job is not running
Add output logging to the cron job
Right now, if your cron job fails silently, you have no way to know what went wrong. Cron does not display terminal output anywhere by default. Redirecting stdout and stderr to a log file gives you a debugging trail.
- Open your crontab for editing again by running:
crontab -e
- Find your existing cron entry. It looks like this:
*/5 * * * * /home/[[USERNAME="your WSL username"]]/log-monitor/scripts/monitor.sh
- Replace that entire line with this updated version that appends output to a log file:
*/5 * * * * /home/[[USERNAME="your WSL username"]]/log-monitor/scripts/monitor.sh >> /home/[[USERNAME="your WSL username"]]/log-monitor/reports/cron.log 2>&1
What does the redirect syntax do?
- >> appends stdout (normal output) to cron.log instead of overwriting it each run.
- 2>&1 redirects stderr (error messages) into the same file as stdout. This means both regular output and error messages end up in one place for easy debugging.
- Save and close the file by pressing Ctrl+O, then Enter, then Ctrl+X.
- Verify the updated entry is saved by running:
crontab -l
You should see the full line including the >> ...cron.log 2>&1 redirect at the end.
- After the next cron execution (wait for the interval to pass), check the output log by running:
cat ~/log-monitor/reports/cron.log
You should see the "Report generated" message from your script, confirming cron executed monitor.sh successfully and captured its output.
cron.log is empty or missing?
If the file does not exist yet, the cron job may not have fired since your edit. Wait for the full interval to pass and check again.
If the file exists but contains error messages, check that the paths in your crontab entry match your actual username and directory structure exactly. Run ls ~/log-monitor/scripts/monitor.sh to confirm the script is where you expect.
Help me troubleshoot my cron output log
✔️ Awesome, I've got everything!
Great. Your cron job is registered, firing on schedule, and logging its output for debugging. Make sure the crontab entry uses */5 if you changed it to */1 earlier for testing.
ⓧ I'd like to double check the full crontab entry
Run crontab -l and confirm your output matches this (with your actual username substituted):
*/5 * * * * /home/[[USERNAME="your WSL username"]]/log-monitor/scripts/monitor.sh >> /home/[[USERNAME="your WSL username"]]/log-monitor/reports/cron.log 2>&1
Secret mission
Multi-Log Monitoring with Color-Coded Output
Your monitor works great on a single file. But production systems generate logs from dozens of services. Can you extend your script to scan an entire directory of log files and print color-coded severity output to the terminal?
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 in WSL, so there are no ongoing cloud costs.
Resources you used:
- ~/log-monitor/ directory (scripts, logs, and reports).
- Cron job running monitor.sh every 5 minutes.
- Cron service (installed via apt).
Keep everything running
No action needed. Choose this if you are still actively building or want the monitor to keep generating reports on a schedule.
- Your cron job will continue to run monitor.sh every 5 minutes while WSL is open.
- Reports will keep accumulating in ~/log-monitor/reports/.
- The cron service stops automatically when you close your WSL terminal, so there is no background cost or resource drain when WSL is shut down.
Pause - I'll come back to this later
Remove the scheduled cron job so the script stops running automatically. Your project files stay on disk for when you return.
- Remove your cron job by running:
crontab -r
- Verify it is removed by running:
crontab -l
- You should see a message saying no crontab is installed for your user.
- Your ~/log-monitor/ directory and all scripts remain intact for next time.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes the cron job, all project files, and optionally the cron package itself.
- Remove the cron job by running:
crontab -r
- Delete the entire project directory by running:
rm -rf ~/log-monitor
- Confirm the directory is gone by running:
ls ~/log-monitor
- You should see an error saying the directory does not exist.
- Optionally, uninstall the cron package if you no longer need it:
sudo apt remove -y cron
Keep cron if you plan to do more Linux projects
Cron is a standard Linux utility used in many DevOps workflows. If you plan to continue learning Linux or automation, it is worth keeping installed.
Nice Work!
Nice Work!
You just built a fully automated server log monitoring system from scratch using nothing but Bash. This is exactly the kind of production tooling that DevOps teams rely on every day.
You've learned how to:
- Build a Bash monitoring script that uses grep, awk, sort, uniq, pipes, and redirection to parse server logs and generate error reports.
- Implement functions, conditionals, and threshold-based alerting with proper scripting practices like shebang lines, safety flags, and error handling.
- Automate the entire workflow with cron scheduling so the monitor runs every 5 minutes without human intervention.
- Secret Mission: Extended the system to monitor multiple log files with color-coded severity output.
Ready to quiz yourself?