Server--:--:--You--:--:--

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 nagios user has a different PATH/env, no network policy exception, or can't read a cert/socket. Always test with sudo -u nagios.
  • Plugin hangs → Nagios kills it (CRITICAL "timed out") — no internal timeout. Always pass --max-time / timeout to network calls; Nagios's own service_check_timeout is the backstop.
  • Perfdata not graphing — format error. Exactly label=value;warn;crit;min;max after 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_attempts so a transient blip needs N consecutive fails.
  • Exit 127 / "command not found" — wrong interpreter path, CRLF line endings (file script.sh), or not executable.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.