Writing a Custom Nagios Plugin
By Prabath Thalangama· August 28, 2026· 4 min read
#nagios#monitoring#plugins
Introduction¶
Nagios (and Icinga, Naemon, Shinken, and check_mk's classic mode) run plugins — small executables that check one thing and report status via an exit code and a line of text. Anything that follows the convention works: bash, Python, Go, a compiled binary.
The contract¶
Exit code = status:
| Code | Status | Meaning |
|---|---|---|
| 0 | OK | all good |
| 1 | WARNING | degraded, look soon |
| 2 | CRITICAL | broken, act now |
| 3 | UNKNOWN | the check itself failed (can't connect, bad args) |
stdout = the message, first line shown in the UI and notifications:
OK - queue depth 42 | queue=42;100;500;0;
Everything after the | is performance data for graphing:
label=value[UOM];warn;crit;min;max. Multiple space-separated metrics allowed.
Keep the text short, lead with the status word, put the number that matters in it.
Bash example: check a queue depth over HTTP¶
#!/usr/bin/env bash
set -euo pipefail
WARN=100
CRIT=500
URL="http://localhost:8080/metrics/queue"
TIMEOUT=10
usage() { echo "usage: $0 -w WARN -c CRIT -u URL"; exit 3; }
while getopts "w:c:u:t:h" opt; do
case "$opt" in
w) WARN=$OPTARG ;;
c) CRIT=$OPTARG ;;
u) URL=$OPTARG ;;
t) TIMEOUT=$OPTARG ;;
*) usage ;;
esac
done
depth=$(curl -fsS --max-time "$TIMEOUT" "$URL" 2>/dev/null) || {
echo "UNKNOWN - could not fetch $URL"
exit 3
}
if ! [[ "$depth" =~ ^[0-9]+$ ]]; then
echo "UNKNOWN - unexpected response: $depth"
exit 3
fi
perf="queue=${depth};${WARN};${CRIT};0;"
if (( depth >= CRIT )); then echo "CRITICAL - queue depth ${depth} | ${perf}"; exit 2
elif (( depth >= WARN )); then echo "WARNING - queue depth ${depth} | ${perf}"; exit 1
else echo "OK - queue depth ${depth} | ${perf}"; exit 0
fi
Python example: check certificate expiry¶
#!/usr/bin/env python3
import argparse, socket, ssl, sys
from datetime import datetime, timezone
OK, WARNING, CRITICAL, UNKNOWN = 0, 1, 2, 3
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("-H", "--host", required=True)
p.add_argument("-p", "--port", type=int, default=443)
p.add_argument("-w", "--warn", type=int, default=21, help="days")
p.add_argument("-c", "--crit", type=int, default=7, help="days")
p.add_argument("-t", "--timeout", type=float, default=10.0)
a = p.parse_args()
ctx = ssl.create_default_context()
try:
with socket.create_connection((a.host, a.port), timeout=a.timeout) as sock:
with ctx.wrap_socket(sock, server_hostname=a.host) as ss:
not_after = ss.getpeercert()["notAfter"]
except Exception as e: # noqa: BLE001 - report anything as UNKNOWN
print(f"UNKNOWN - {a.host}:{a.port} - {e}")
return UNKNOWN
expiry = datetime.strptime(not_after, "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
days = (expiry - datetime.now(timezone.utc)).days
perf = f"days={days};{a.warn};{a.crit};;"
if days <= a.crit:
print(f"CRITICAL - cert for {a.host} expires in {days}d ({not_after}) | {perf}")
return CRITICAL
if days <= a.warn:
print(f"WARNING - cert for {a.host} expires in {days}d | {perf}")
return WARNING
print(f"OK - cert for {a.host} valid {days}d | {perf}")
return OK
if __name__ == "__main__":
sys.exit(main())
Registering the check¶
# commands.cfg
define command {
command_name check_queue_depth
command_line $USER1$/check_queue_depth.sh -u $ARG1$ -w $ARG2$ -c $ARG3$
}
# service definition
define service {
use generic-service
host_name app01
service_description Job queue depth
check_command check_queue_depth!http://localhost:8080/metrics/queue!100!500
}
Drop the script in $USER1$ (usually /usr/lib/nagios/plugins/), chmod +x,
reload Nagios.
Verification and troubleshooting¶
# Run it exactly as Nagios would, as the nagios user
sudo -u nagios /usr/lib/nagios/plugins/check_queue_depth.sh -u http://localhost:8080/metrics/queue -w 100 -c 500
echo $? # 0/1/2/3
# Nagios-side
nagios -v /etc/nagios/nagios.cfg # config check before reload
- Check works by hand, UNKNOWN in Nagios — the
nagiosuser has a different PATH/env, no network policy exception, or can't read a cert/socket. Always test withsudo -u nagios. - Plugin hangs → Nagios kills it (CRITICAL "timed out") — no internal
timeout. Always pass
--max-time/timeoutto network calls; Nagios's ownservice_check_timeoutis the backstop. - Perfdata not graphing — format error. Exactly
label=value;warn;crit;min;maxafter a single|, value must have a unit or be bare number, no spaces around=. - Flapping — thresholds too close to normal operating range, or a noisy
metric. Widen the band, add
check_interval/retry_interval+max_check_attemptsso a transient blip needs N consecutive fails. - Exit 127 / "command not found" — wrong interpreter path, CRLF line endings
(
file script.sh), or not executable.
Related tools and reading¶
- On-site: SSL Certificate Checker, Port Checker.
- Related posts: Nagios active vs passive checks, Nagios notifications and escalations.
Related tools
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.