#!/bin/bash # /usr/lib/check_mk_agent/local/300/check_pm2 set -u CONFIG="/etc/check_mk/pm2.conf" OK=0 WARN=1 CRIT=2 UNKNOWN=3 # # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- # if [[ ! -r "$CONFIG" ]]; then printf '%s\n' "${UNKNOWN} \"PM2\" - Configuration file ${CONFIG} missing or unreadable" exit 0 fi # shellcheck disable=SC1090 source "$CONFIG" PM2_USER="${PM2_USER:-root}" PM2_HOME="${PM2_HOME:-}" CHECK_PM2_UPDATES="${CHECK_PM2_UPDATES:-yes}" CHECK_NODE_UPDATES="${CHECK_NODE_UPDATES:-yes}" PM2_PACKAGE_URL="${PM2_PACKAGE_URL:-https://registry.npmjs.org/pm2/latest}" NODE_RELEASES_URL="${NODE_RELEASES_URL:-https://nodejs.org/dist/index.json}" # # --------------------------------------------------------------------------- # Requirements # --------------------------------------------------------------------------- # if ! command -v jq >/dev/null 2>&1; then printf '%s\n' "${UNKNOWN} \"PM2\" - jq is not installed" exit 0 fi if ! command -v curl >/dev/null 2>&1; then printf '%s\n' "${UNKNOWN} \"PM2\" - curl is not installed" exit 0 fi if ! id "$PM2_USER" >/dev/null 2>&1; then printf '%s\n' "${UNKNOWN} \"PM2\" - Configured PM2 user ${PM2_USER} does not exist" exit 0 fi # # --------------------------------------------------------------------------- # User / PM2 environment # --------------------------------------------------------------------------- # USER_HOME="$( getent passwd "$PM2_USER" | cut -d: -f6 )" if [[ -z "$USER_HOME" ]]; then printf '%s\n' "${UNKNOWN} \"PM2\" - Unable to determine home directory for ${PM2_USER}" exit 0 fi if [[ -z "$PM2_HOME" ]]; then PM2_HOME="${USER_HOME}/.pm2" fi # # --------------------------------------------------------------------------- # Locate PM2 # --------------------------------------------------------------------------- # PM2_BIN="" for candidate in \ /usr/bin/pm2 \ /usr/local/bin/pm2 \ "${USER_HOME}/.local/bin/pm2" do if [[ -x "$candidate" ]]; then PM2_BIN="$candidate" break fi done if [[ -z "$PM2_BIN" ]]; then PM2_BIN="$(command -v pm2 2>/dev/null || true)" fi # # NVM fallback # if [[ -z "$PM2_BIN" && -d "${USER_HOME}/.nvm/versions/node" ]]; then PM2_BIN="$( find "${USER_HOME}/.nvm/versions/node" \ -mindepth 3 \ -maxdepth 3 \ -type f \ -path '*/bin/pm2' \ -perm -u+x \ 2>/dev/null | sort -V | tail -n 1 )" fi if [[ -z "$PM2_BIN" || ! -x "$PM2_BIN" ]]; then printf '%s\n' "${UNKNOWN} \"PM2\" - Unable to locate PM2 executable" exit 0 fi PM2_BIN_DIR="$(dirname "$PM2_BIN")" # # Node used by this PM2 installation. # NODE_BIN="${PM2_BIN_DIR}/node" if [[ ! -x "$NODE_BIN" ]]; then NODE_BIN="$(command -v node 2>/dev/null || true)" fi # # --------------------------------------------------------------------------- # Retrieve PM2 process list # --------------------------------------------------------------------------- # if [[ "$PM2_USER" == "root" ]]; then PM2_JSON="$( PM2_HOME="$PM2_HOME" \ PATH="${PM2_BIN_DIR}:${PATH}" \ "$PM2_BIN" jlist 2>/dev/null )" PM2_RC=$? else PM2_JSON="$( runuser -u "$PM2_USER" -- \ env \ PM2_HOME="$PM2_HOME" \ PATH="${PM2_BIN_DIR}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \ "$PM2_BIN" jlist 2>/dev/null )" PM2_RC=$? fi if [[ $PM2_RC -ne 0 || -z "$PM2_JSON" ]]; then printf '%s\n' "${CRIT} \"PM2\" - Unable to retrieve PM2 process list for user ${PM2_USER}" exit 0 fi if ! printf '%s' "$PM2_JSON" | jq -e 'type == "array"' >/dev/null 2>&1; then printf '%s\n' "${UNKNOWN} \"PM2\" - PM2 returned invalid JSON" exit 0 fi PROCESS_COUNT="$( printf '%s' "$PM2_JSON" | jq 'length' )" if [[ "$PROCESS_COUNT" -eq 0 ]]; then printf '%s\n' "${WARN} \"PM2\" processes=0 No processes managed by PM2 for user ${PM2_USER}" exit 0 fi # # --------------------------------------------------------------------------- # Overall PM2 state # --------------------------------------------------------------------------- # ONLINE_COUNT="$( printf '%s' "$PM2_JSON" | jq '[.[] | select(.pm2_env.status == "online")] | length' )" OFFLINE_COUNT=$((PROCESS_COUNT - ONLINE_COUNT)) if [[ "$OFFLINE_COUNT" -eq 0 ]]; then printf '%s\n' \ "${OK} \"PM2\" processes=${PROCESS_COUNT};;;0|online=${ONLINE_COUNT};;;0|offline=0;;;0 ${PROCESS_COUNT}/${PROCESS_COUNT} processes online" else printf '%s\n' \ "${CRIT} \"PM2\" processes=${PROCESS_COUNT};;;0|online=${ONLINE_COUNT};;;0|offline=${OFFLINE_COUNT};;;0 ${ONLINE_COUNT}/${PROCESS_COUNT} processes online, ${OFFLINE_COUNT} not online" fi # # --------------------------------------------------------------------------- # PM2 version / update # --------------------------------------------------------------------------- # PM2_VERSION="$( PATH="${PM2_BIN_DIR}:${PATH}" \ "$PM2_BIN" --version 2>/dev/null | tail -n 1 | tr -d '[:space:]' )" if [[ "$CHECK_PM2_UPDATES" == "yes" ]]; then if [[ -z "$PM2_VERSION" ]]; then printf '%s\n' "${UNKNOWN} \"PM2 Update\" - Unable to determine installed PM2 version" else PM2_RELEASE_RESPONSE="$( curl \ --silent \ --show-error \ --fail \ --connect-timeout 3 \ --max-time 8 \ "$PM2_PACKAGE_URL" \ 2>/dev/null )" CURL_RC=$? if [[ $CURL_RC -ne 0 || -z "$PM2_RELEASE_RESPONSE" ]]; then printf '%s\n' \ "${UNKNOWN} \"PM2 Update\" - Installed ${PM2_VERSION}, unable to query npm registry" elif ! printf '%s' "$PM2_RELEASE_RESPONSE" | jq -e . >/dev/null 2>&1; then printf '%s\n' \ "${UNKNOWN} \"PM2 Update\" - npm registry returned invalid JSON" else PM2_LATEST="$( printf '%s' "$PM2_RELEASE_RESPONSE" | jq -r '.version // empty' )" if [[ -z "$PM2_LATEST" ]]; then printf '%s\n' \ "${UNKNOWN} \"PM2 Update\" - Unable to determine latest PM2 version" else NEWEST="$( printf '%s\n%s\n' \ "$PM2_VERSION" \ "$PM2_LATEST" | sort -V | tail -n 1 )" if [[ "$PM2_VERSION" == "$PM2_LATEST" ]]; then printf '%s\n' \ "${OK} \"PM2 Update\" - Current: installed ${PM2_VERSION}, latest ${PM2_LATEST}" elif [[ "$NEWEST" == "$PM2_LATEST" ]]; then printf '%s\n' \ "${WARN} \"PM2 Update\" - Update available: installed ${PM2_VERSION}, latest ${PM2_LATEST}" else printf '%s\n' \ "${OK} \"PM2 Update\" - Installed ${PM2_VERSION} is newer than latest ${PM2_LATEST}" fi fi fi fi fi # # --------------------------------------------------------------------------- # Node.js / NVM version # --------------------------------------------------------------------------- # if [[ "$CHECK_NODE_UPDATES" == "yes" ]]; then if [[ -z "$NODE_BIN" || ! -x "$NODE_BIN" ]]; then printf '%s\n' "${UNKNOWN} \"Node.js Update\" - Unable to locate Node.js executable" else NODE_VERSION="$( "$NODE_BIN" --version 2>/dev/null | sed 's/^v//' )" if [[ -z "$NODE_VERSION" ]]; then printf '%s\n' "${UNKNOWN} \"Node.js Update\" - Unable to determine installed Node.js version" else NODE_RELEASES="$( curl \ --silent \ --show-error \ --fail \ --connect-timeout 3 \ --max-time 8 \ "$NODE_RELEASES_URL" \ 2>/dev/null )" CURL_RC=$? if [[ $CURL_RC -ne 0 || -z "$NODE_RELEASES" ]]; then printf '%s\n' \ "${UNKNOWN} \"Node.js Update\" - Installed ${NODE_VERSION}, unable to query Node.js releases" elif ! printf '%s' "$NODE_RELEASES" | jq -e 'type == "array"' >/dev/null 2>&1; then printf '%s\n' \ "${UNKNOWN} \"Node.js Update\" - Node.js release API returned invalid data" else INSTALLED_MAJOR="${NODE_VERSION%%.*}" LATEST_CURRENT="$( printf '%s' "$NODE_RELEASES" | jq -r ' [ .[] | select(.lts == false) | .version ][0] // empty ' | sed 's/^v//' )" LATEST_LTS="$( printf '%s' "$NODE_RELEASES" | jq -r ' [ .[] | select(.lts != false) | .version ][0] // empty ' | sed 's/^v//' )" LATEST_SAME_MAJOR="$( printf '%s' "$NODE_RELEASES" | jq -r \ --arg prefix "v${INSTALLED_MAJOR}." ' [ .[] | select(.version | startswith($prefix)) | .version ][0] // empty ' | sed 's/^v//' )" SAME_MAJOR_LTS="$( printf '%s' "$NODE_RELEASES" | jq -r \ --arg prefix "v${INSTALLED_MAJOR}." ' [ .[] | select(.version | startswith($prefix)) | .lts ][0] // false ' )" # # If no releases for the installed major exist in the current # Node.js release index, consider it unsupported/EOL. # if [[ -z "$LATEST_SAME_MAJOR" ]]; then printf '%s\n' \ "${WARN} \"Node.js Update\" - Installed ${NODE_VERSION} appears unsupported/EOL; latest LTS ${LATEST_LTS}, current ${LATEST_CURRENT}" elif [[ "$SAME_MAJOR_LTS" == "false" && "$INSTALLED_MAJOR" -lt "${LATEST_CURRENT%%.*}" ]]; then printf '%s\n' \ "${WARN} \"Node.js Update\" - Installed ${NODE_VERSION} is on a non-LTS release line; latest LTS ${LATEST_LTS}, current ${LATEST_CURRENT}" elif [[ "$NODE_VERSION" != "$LATEST_SAME_MAJOR" ]]; then NEWEST="$( printf '%s\n%s\n' \ "$NODE_VERSION" \ "$LATEST_SAME_MAJOR" | sort -V | tail -n 1 )" if [[ "$NEWEST" == "$LATEST_SAME_MAJOR" ]]; then printf '%s\n' \ "${WARN} \"Node.js Update\" - Update available in Node ${INSTALLED_MAJOR}.x: installed ${NODE_VERSION}, latest ${LATEST_SAME_MAJOR}; latest LTS ${LATEST_LTS}" else printf '%s\n' \ "${OK} \"Node.js Update\" - Installed ${NODE_VERSION}; latest LTS ${LATEST_LTS}, current ${LATEST_CURRENT}" fi else printf '%s\n' \ "${OK} \"Node.js Update\" - Current on Node ${INSTALLED_MAJOR}.x: ${NODE_VERSION}; latest LTS ${LATEST_LTS}, current ${LATEST_CURRENT}" fi fi fi fi fi # # --------------------------------------------------------------------------- # Individual PM2 processes # --------------------------------------------------------------------------- # while IFS= read -r PROCESS; do NAME="$( printf '%s' "$PROCESS" | jq -r '.name // "unknown"' )" STATUS="$( printf '%s' "$PROCESS" | jq -r '.pm2_env.status // "unknown"' )" PID="$( printf '%s' "$PROCESS" | jq -r '.pid // 0' )" RESTARTS="$( printf '%s' "$PROCESS" | jq -r '.pm2_env.restart_time // 0' )" CRON_RESTART="$( printf '%s' "$PROCESS" | jq -r '.pm2_env.cron_restart // empty' )" AUTORESTART="$( printf '%s' "$PROCESS" | jq -r '.pm2_env.autorestart // true' )" CPU="$( printf '%s' "$PROCESS" | jq -r '.monit.cpu // 0' )" MEMORY_BYTES="$( printf '%s' "$PROCESS" | jq -r '.monit.memory // 0' )" START_TIME="$( printf '%s' "$PROCESS" | jq -r '.pm2_env.pm_uptime // 0' )" NAME="${NAME//\"/\'}" MEMORY_MB="$( awk -v bytes="$MEMORY_BYTES" \ 'BEGIN { printf "%.1f", bytes / 1024 / 1024 }' )" NOW_MS=$(( $(date +%s) * 1000 )) if [[ "$START_TIME" =~ ^[0-9]+$ && "$START_TIME" -gt 0 ]]; then UPTIME_SECONDS="$( awk -v now="$NOW_MS" -v start="$START_TIME" \ 'BEGIN { diff = (now - start) / 1000 if (diff < 0) diff = 0 printf "%.0f", diff }' )" else UPTIME_SECONDS=0 fi DAYS=$((UPTIME_SECONDS / 86400)) HOURS=$(((UPTIME_SECONDS % 86400) / 3600)) MINUTES=$(((UPTIME_SECONDS % 3600) / 60)) if [[ "$DAYS" -gt 0 ]]; then UPTIME_TEXT="${DAYS}d ${HOURS}h ${MINUTES}m" elif [[ "$HOURS" -gt 0 ]]; then UPTIME_TEXT="${HOURS}h ${MINUTES}m" else UPTIME_TEXT="${MINUTES}m" fi STATE=$OK STATE_TEXT="Online" if [[ "$STATUS" != "online" ]]; then STATE=$CRIT STATE_TEXT="Status ${STATUS}" fi if [[ -n "$CRON_RESTART" ]]; then RESTART_INFO="restarts ${RESTARTS}, scheduled restart ${CRON_RESTART}" else RESTART_INFO="restarts ${RESTARTS}" fi if [[ "$AUTORESTART" == "false" ]]; then RESTART_INFO="${RESTART_INFO}, autorestart disabled" fi PERFDATA="cpu=${CPU}%;;;0;100|memory=${MEMORY_BYTES}B;;;0|restarts=${RESTARTS};;;0|uptime=${UPTIME_SECONDS}s;;;0" printf '%s\n' \ "${STATE} \"PM2 ${NAME}\" ${PERFDATA} ${STATE_TEXT} - PID ${PID}, uptime ${UPTIME_TEXT}, CPU ${CPU}%, memory ${MEMORY_MB} MiB, ${RESTART_INFO}" done < <( printf '%s' "$PM2_JSON" | jq -c '.[]' ) exit 0