## Intro Bash monitoring and alerting are critical for maintaining the health and reliability of Linux systems, applications, and infrastructure. While modern observability stacks offer sophisticated dashboards and alert managers, Bash remains an indispensable tool for quick checks, custom metrics, and lightweight automation. It is the glue that connects disparate systems, and a well-crafted Bash script can detect issues before they escalate into outages. This guide is designed for developers, DevOps consultants, and technical startup teams who need to implement practical monitoring and alerting using Bash. It covers the full lifecycle: from inventorying your environment and safely configuring monitoring scripts, to verifying diagnostics, handling failures, and following an operational checklist. Every recommendation is version-scoped, observable, and reversible where possible. You will find concrete commands, configuration snippets, and examples with expected outputs, so you can adapt them to your own infrastructure. Throughout the article, we emphasize operational safety: observe before changing, limit the blast radius, use placeholders instead of real secrets, verify results, and document recovery paths. We also discuss common pitfalls that trip up even experienced administrators, and how to avoid them. By the end, you will have a robust framework for Bash-based monitoring and alerting that you can deploy with confidence. ## Version and Environment Inventory Before writing a single monitoring script, you must understand your environment. A reliable inventory tells you what is installed, which versions you are running, and how components are connected. This information is essential for choosing the right commands, interpreting output, and troubleshooting when something goes wrong. Start by identifying the operating system and kernel version. These commands are read-only and safe to run on any system: uname -a cat /etc/os-release Example output from uname -a : Linux monitor-prod-01 5.15.0-91-generic #101-Ubuntu SMP Tue Nov 14 13:30:08 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux This tells you the kernel version (5.15), distribution (Ubuntu), and architecture (x86_64). The /etc/os-release file provides distribution details: NAME="Ubuntu" VERSION="20.04.6 LTS (Focal Fossa)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 20.04.6 LTS" VERSION_ID="20.04" Knowing the exact OS version is crucial because command syntax and available tools vary between releases. For example, ss replaced netstat in newer distributions, and systemd is not available on older init systems. Next, inventory the key monitoring tools themselves. Check whether common utilities are installed and their versions: bash --version | head -1 curl --version | head -1 jq --version awk --version | head -1 If any tool is missing, note it. Monitoring scripts often rely on curl for API checks, jq for JSON parsing, and awk for text processing. Install missing packages using your package manager only after verifying the change is safe. For systems running containerized workloads, check Docker and Docker Compose versions: docker --version docker compose version Example output: Docker version 24.0.7, build afdd53b Docker Compose version v2.21.0 If you are using Kubernetes, verify cluster access and version: kubectl version --short This inventories the client and server versions, which is essential for API compatibility. Use these inventory commands to document your baseline. Store the output in a version-controlled file or a configuration management database. This baseline will help you detect drift and troubleshoot issues later. When writing monitoring scripts, always use explicit placeholders for hostnames, paths, and credentials. For example, instead of hardcoding https://api.production.example.com , use a variable like API_ENDPOINT="https://${API_HOST}:${API_PORT}/health" . This makes scripts portable and prevents accidental exposure of sensitive information. ## Safe Configuration Path Once you understand your environment, you need a safe configuration path for your monitoring scripts. This means separating observation from intervention, protecting credentials, and making small, reversible changes. A well-designed configuration path reduces the risk of breaking production systems while you set up monitoring. Start by creating a dedicated directory for your monitoring scripts and configuration files. Use a non-root user with limited permissions: sudo mkdir -p /opt/monitoring/scripts sudo chown -R monitor:monitor /opt/monitoring Then, create a configuration file that stores environment-specific values. This file should be readable only by the monitoring user: sudo touch /opt/monitoring/config.env sudo chmod 600 /opt/monitoring/config.env sudo chown monitor:monitor /opt/monitoring/config.env Inside config.env , define variables for endpoints, thresholds, and notification targets. Never store real credentials in plain text; use environment variables or a secrets manager. Example configuration file: # /opt/monitoring/config.env API_HOST="api.example.com" API_PORT="443" DISK_THRESHOLD_PERCENT="85" CPU_THRESHOLD_PERCENT="90" ALERT_EMAIL="ops@example.com" Your monitoring scripts should source this file to load variables: #!/bin/bash # /opt/monitoring/scripts/check_api.sh source /opt/monitoring/config.env response=$(curl -s -o /dev/null -w "%{http_code}" "https://${API_HOST}:${API_PORT}/health") if [ "$response" -ne 200 ]; then echo "API health check failed with HTTP $response" exit 1 fi echo "API health check passed" This script is read-only: it makes an HTTP GET request and checks the status code. It does not modify any state. The configuration file separates settings from code, making it easier to manage across environments. When you need to make a change, follow this pattern: - Back up the current configuration before editing: cp /opt/monitoring/config.env /opt/monitoring/config.env.bak.$(date +%Y%m%d) - Edit one scoped item at a time. For example, if you need to change the disk threshold, modify only that line. - Verify the change by running the script in a test mode. Add a --dry-run flag to your scripts to simulate actions without side effects. - Document the recovery path : if the change causes unexpected behavior, restore the backup: cp /opt/monitoring/config.env.bak.20231015 /opt/monitoring/config.env For containerized environments, ensure your monitoring scripts are included in the container image or mounted as a volume. In Docker Compose, you can define a monitoring service that runs checks periodically: # docker-compose.yml snippet services: monitor: image: alpine:latest volumes: - /opt/monitoring/scripts:/scripts - /opt/monitoring/config.env:/config.env:ro command: ["/bin/sh", "/scripts/check_api.sh"] environment: - CONFIG_FILE=/config.env This keeps monitoring close to the application and simplifies deployment. Always test configuration changes in a staging environment before applying them to production. ## Verification and Diagnostics Verification and diagnostics are the heart of any monitoring system. You need to know not only that something is wrong, but why it is wrong and how to confirm it. Bash excels at running quick, targeted checks that reveal the state of your system. Let's start with common system health checks. Disk space is a frequent source of problems. Use df to check usage: df -h / Expected output: Filesystem Size Used Avail Use% Mounted on /dev/sda1 50G 20G 28G 42% / To automatically alert when disk usage exceeds a threshold, write a script: #!/bin/bash THRESHOLD=85 USAGE=$(df -h / | awk 'NR==2 {gsub("%","",$5); print $5}') if [ "$USAGE" -ge "$THRESHOLD" ]; then echo "Disk usage is ${USAGE}%, exceeding threshold ${THRESHOLD}%" exit 1 else echo "Disk usage is ${USAGE}%, within limit" fi Run it: ./check_disk.sh Output when safe: Disk usage is 42%, within limit When threshold exceeded: Disk usage is 87%, exceeding threshold 85% The script exits with code 1 on failure, which is useful for integration with cron or CI systems. CPU load is another critical metric. Use uptime to see load averages: uptime Output: 14:32:10 up 30 days, 5:12, 1 user, load average: 0.80, 0.60, 0.55 The three numbers represent load average over 1, 5, and 15 minutes. Compare these to the number of CPU cores. A load average consistently above the core count indicates overloading. Use nproc to get core count: nproc # Output: 4 A script can check if the 1-minute load average exceeds 80% of core count: #!/bin/bash CORES=$(nproc) LOAD=$(uptime | awk -F'load average:' '{print $2}' | cut -d, -f1 | tr -d ' ') THRESHOLD=$(echo "$CORES * 0.8" | bc) if (( $(echo "$LOAD > $THRESHOLD" | bc -l) )); then echo "High load: $LOAD exceeds threshold $THRESHOLD" exit 1 fi echo "Load normal: $LOAD" This uses bc for floating-point arithmetic. Ensure bc is installed or use integer math. Memory usage can be checked with free : free -h Output: total used free shared buff/cache available Mem: 7.7Gi 2.1Gi 3.2Gi 120Mi 2.4Gi 5.3Gi Swap: 2.0Gi 0B 2.0Gi The available column is key: it shows memory available for new processes. Alert if available memory falls below a threshold, say 10% of total. For application-specific checks, simulate what a user would do. If you have a web service, use curl to test response time and status: curl -o /dev/null -s -w 'HTTP %{http_code} in %{time_total}s\n' https://api.example.com/health Output: HTTP 200 in 0.321s Log diagnostics are essential. Check recent errors in system logs or application logs. Use journalctl for systemd services: journalctl -u myapp.service -n 20 --no-pager If you see repeated errors, investigate further. You can also use grep to search for specific patterns: grep -i "error" /var/log/myapp/app.log | tail -10 Always correlate diagnostics with metrics. For example, if disk usage is high, find which files are consuming space: sudo du -ah /var/log | sort -rh | head -10 This shows the largest files and directories, helping you decide what to rotate or delete. ## Failure Modes and Recovery No monitoring system is perfect. Things fail: scripts crash, thresholds are misconfigured, alerts go missing. Understanding common failure modes and having a recovery plan is crucial for operational resilience. Failure mode 1: Script exits unexpectedly due to missing dependencies. Your monitoring script may rely on jq or curl , but those tools may not be installed on a new server. The script fails with a "command not found" error and no alert is sent. Recovery : Add dependency checks at the beginning of every script. Use a function to verify required commands: #!/bin/bash REQUIRED_CMDS=("curl" "jq" "awk") for cmd in "${REQUIRED_CMDS[@]}"; do if ! command -v $cmd &> /dev/null; then echo "Error: $cmd is not installed" exit 127 fi done This ensures a clear error message and non-zero exit code. Include installation instructions in your runbook. Failure mode 2: Thresholds are too sensitive or too lenient. If your disk threshold is set to 70% on a system that routinely uses 75% for batch jobs, you will get false alarms. Conversely, if it's set to 95%, you may run out of space before an alert triggers. Recovery : Baseline your metrics over a period (e.g., 30 days) to understand normal ranges. Use tools like sar or custom collection scripts to gather historical data. Then set thresholds with hysteresis: alert when usage exceeds 85% for three consecutive checks, and clear when it drops below 80%. This avoids flapping. Failure mode 3: Alerts are not delivered. Your script sends an email via mailx , but the SMTP server is misconfigured or the email is marked as spam. You think everything is fine, but no one receives alerts. Recovery : Test alert delivery regularly. Use a dedicated alerting mechanism like a webhook to a chat platform (Slack, Discord) or a push notification service. For example, send a JSON payload to a Slack webhook: #!/bin/bash WEBHOOK_URL="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" MESSAGE="Disk usage critical on $(hostname)" curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$MESSAGE\"}" "$WEBHOOK_URL" Store the webhook URL in a secure config file, not in the script. Test the webhook independently before relying on it. Failure mode 4: Monitoring script itself consumes too many resources. A poorly written script that runs every minute and spawns multiple processes can add load to the system, exacerbating the very problems it is supposed to detect. Recovery : Optimize scripts. Use built-in Bash features instead of external commands where possible. For example, use [[ ]] for tests instead of calling test . Use awk efficiently and avoid pipes when you can. Schedule checks with cron at appropriate intervals: disk checks every 15 minutes, API checks every minute if needed. Monitor the monitor: check the script's own resource usage with time or ps . Failure mode 5: Configuration drift. Over time, servers get patched, applications update, and paths change. Your monitoring scripts may reference old paths or versions, leading to false negatives. Recovery : Treat monitoring configuration as code. Store scripts and configs in version control (Git). Use configuration management tools (Ansible, Puppet, Chef) to deploy and update them. Run a periodic audit script that compares the deployed configuration with the intended state from version control. Always have a rollback plan. Keep backups of previous configurations and scripts. Document the exact steps to restore service if a change breaks monitoring. Test recovery procedures in a staging environment. ## Operations Checklist To ensure your Bash monitoring system remains effective, follow a routine operations checklist. This checklist helps you catch issues early, maintain consistency, and continuously improve. Assign a single owner to each item to ensure accountability. Review the checklist weekly or after any significant infrastructure change. Here is a practical checklist you can adapt:
| Check | Command / Method | Expected Result | Owner | Frequency |
|---|---|---|---|---|
| Disk usage on root | df -h / | Usage below 85% | Priya Shah, Sysadmin | Daily |
| CPU load average | uptime | 1-min load < 80% of cores | Priya Shah | Daily |
| Memory available | free -h | Available > 10% of total | Priya Shah | Daily |
| API health endpoint | curl -s -o /dev/null -w "%{http_code}" https://api.example.com/health | HTTP 200 | Marcus Chen, DevOps | Every 15 min |
| Critical log errors | grep -i "error" /var/log/myapp/app.log | tail -10 | No new critical errors | Elena Rodriguez, App Support | Hourly |
| Alert delivery test | Send test webhook/email | Receives test alert | Marcus Chen | Weekly |
| Monitoring script syntax | bash -n /opt/monitoring/scripts/*.sh | No syntax errors | Priya Shah | After any change |
| Config file permissions | ls -l /opt/monitoring/config.env | -rw------- (600) | Priya Shah | Monthly |
| Dependency availability | for cmd in curl jq awk; do command -v $cmd; done | All commands found | Elena Rodriguez | Monthly |
| Review thresholds | Compare metrics history | Thresholds appropriate | Marcus Chen | Quarterly |