2026年5月29日

2026年5月29日

Pythonでシステム監視スクリプトを書く方法|psutilで閾値チェックと通知

Python で小さなシステム監視を作るなら、psutil で CPU・メモリ・ディスクの値を取り、閾値を超えた状態が続いたときだけ通知するスクリプトを、systemd のタイマーか cron で1分ごとに動かす構成が扱いやすいです。この記事では、値の取り方、誤報を減らす「連続回数」と「状態の記録」、Webhook での通知、定期実行の設定、そして監視スクリプト自身が止まったことに気づく方法までを順に説明します。対象は、VPS や小規模なサーバーを数台運用していて、本格的な監視ツールを入れる前に最低限の見張りを置きたい方です。

監視スクリプトで何を見るか

最初から多くを監視しようとすると、通知が増えて読まれなくなります。まずは「放置するとサービスが止まるもの」に絞ります。

  • ディスク使用率:満杯になるとデータベースやログの書き込みが失敗し、サイトが止まる原因になります。最優先で見ます。
  • メモリ:Linux はキャッシュにメモリを使うため、「使用中」ではなく「すぐ使える量(available)」を基準にします。
  • CPU 使用率:一瞬の高負荷はよくあることなので、数分続いたときだけ問題とみなします。
  • プロセスやサービスの稼働:Web サーバーやデータベースが動いているか。
  • 外から見た応答:HTTP でページが返るか。サーバーの内側の値が正常でも、外から見えないことがあります。

この記事では、上の3つ(ディスク・メモリ・CPU)を中心に、サービス稼働の確認を補助として扱います。

psutil のインストールと値の取り方

psutil は、CPU・メモリ・ディスク・ネットワーク・プロセスの情報を OS をまたいで取得できるライブラリです。仮想環境に入れて使います。

python3 -m venv /opt/sysmon/.venv
/opt/sysmon/.venv/bin/pip install psutil

主な値は次のように取れます。

import psutil

print(psutil.cpu_percent(interval=1))      # 1秒間の CPU 使用率(%)
mem = psutil.virtual_memory()
print(mem.percent, mem.available)          # 使用率(%)と、すぐ使える量(バイト)
print(psutil.disk_usage("/").percent)      # ルートパーティションの使用率(%)
print(psutil.getloadavg())                 # 1分・5分・15分のロードアベレージ

cpu_percent(interval=1) は1秒間待って測るため、呼び出しに1秒かかります。interval を省略すると前回の呼び出しからの差分になり、初回は意味のない値が返るので、単発で実行するスクリプトでは interval を指定してください。

virtual_memory().percent は Linux では available をもとに計算されるため、キャッシュの分は「使用中」に数えられません。free コマンドの used と見比べて数字が違っても、そのためです。

誤報を減らす設計:連続回数と状態の記録

閾値を1回超えただけで通知すると、バックアップやアップデートのたびに通知が飛び、やがて誰も読まなくなります。そこで次の2つを組み合わせます。

  1. 連続回数:閾値超えが3回続いたら通知する。1分間隔なら、約3分続いた高負荷だけを拾えます。
  2. 状態の記録:一度通知したら「通知済み」と記録し、回復するまで同じ通知を繰り返さない。回復したら「戻りました」を1回だけ送る。

スクリプトは1回ごとに終了するため、前回までの回数や通知済みかどうかは JSON ファイルに保存します。

監視スクリプトの例

Webhook の URL は、スクリプトに書かず環境変数から読みます。URL を知っていれば誰でも投稿できる種類の秘密情報なので、リポジトリに入れないでください。

#!/usr/bin/env python3
"""CPU・メモリ・ディスクを確認し、閾値超えが続いたら Webhook で通知する"""
import json
import os
import socket
import sys
import urllib.request
from pathlib import Path

import psutil

THRESHOLDS = {"cpu": 90.0, "memory": 90.0, "disk": 85.0}  # 単位は %
CONSECUTIVE = 3  # この回数続いたら通知する
STATE_FILE = Path(os.environ.get("MONITOR_STATE", "/var/tmp/sysmon_state.json"))
WEBHOOK_URL = os.environ.get("MONITOR_WEBHOOK_URL", "")


def collect() -> dict:
    return {
        "cpu": psutil.cpu_percent(interval=1),
        "memory": psutil.virtual_memory().percent,
        "disk": psutil.disk_usage("/").percent,
    }


def load_state() -> dict:
    try:
        return json.loads(STATE_FILE.read_text(encoding="utf-8"))
    except (FileNotFoundError, json.JSONDecodeError):
        return {}


def save_state(state: dict) -> None:
    tmp = STATE_FILE.with_suffix(".tmp")
    tmp.write_text(json.dumps(state), encoding="utf-8")
    tmp.replace(STATE_FILE)


def notify(text: str) -> bool:
    if not WEBHOOK_URL:
        print(text, file=sys.stderr)
        return True
    body = json.dumps({"text": text}).encode("utf-8")
    req = urllib.request.Request(
        WEBHOOK_URL, data=body, headers={"Content-Type": "application/json"}
    )
    try:
        with urllib.request.urlopen(req, timeout=10) as res:
            res.read()
        return True
    except OSError as e:  # 通信エラーや HTTP エラー
        print(f"通知に失敗しました: {e}", file=sys.stderr)
        return False


def main() -> int:
    host = socket.gethostname()
    values = collect()
    state = load_state()
    for key, limit in THRESHOLDS.items():
        entry = state.get(key, {"count": 0, "alerting": False})
        value = values[key]
        if value >= limit:
            entry["count"] += 1
            if entry["count"] >= CONSECUTIVE and not entry["alerting"]:
                msg = f"[{host}] {key} が {value:.1f}% です(閾値 {limit}%、{CONSECUTIVE}回連続)"
                entry["alerting"] = notify(msg)
        else:
            if entry["alerting"]:
                notify(f"[{host}] {key} が {value:.1f}% に戻りました")
            entry = {"count": 0, "alerting": False}
        state[key] = entry
    save_state(state)
    return 0


if __name__ == "__main__":
    sys.exit(main())

コードの要点です。

  • 通知に失敗したら「通知済み」にしない:notify が False を返したときは alerting が立たないので、次の回にもう一度送ろうとします。
  • 状態ファイルは一時ファイルから置き換える:書き込み中に止まっても、壊れた JSON が残りにくくなります。壊れていた場合も load_state が空の状態から始めます。
  • **送る本文は {"text": ...} の形**:Slack の Incoming Webhook はこの形を受け付けます。Discord や Teams など、ほかのサービスは受け付ける JSON の形が違うため、各サービスの開発者向けドキュメントで確認してください。Webhook の作り方や画面の表記はサービス側で変わることがあります(2026年10月現在)。

閾値の値は目安です。ディスクは「満杯になる前に片付ける時間がある値」、メモリと CPU は「普段のピークより少し上」を、運用しながら調整します。

サービスが動いているかを確認する

Web サーバーやデータベースのような systemd のサービスは、プロセス名で探すより systemctl is-active の終了コードで確かめるほうが誤判定が少なく、書き方もシンプルです。

import subprocess


def service_active(name: str) -> bool:
    result = subprocess.run(["systemctl", "is-active", "--quiet", name], check=False)
    return result.returncode == 0

collect() に "nginx": 0 if service_active("nginx") else 100 のような値を加え、閾値を 50 にすれば、連続回数と状態の記録の仕組みをそのまま使えます(通知の文言は「停止しています」のように別に組み立てると読みやすくなります)。外から見た応答を確かめたい場合は、urllib.request.urlopen にタイムアウトを付けて自分のサイトの URL を開き、ステータスコードを見る処理を足します。

定期実行:systemd タイマーの例

Linux で systemd が使える環境なら、cron よりログの確認がしやすい systemd のタイマーが便利です。監視専用のユーザー(例: sysmon)を作ってから、次の2ファイルを置きます。

# /etc/systemd/system/sysmon.service
[Unit]
Description=System resource check

[Service]
Type=oneshot
User=sysmon
StateDirectory=sysmon
Environment=MONITOR_STATE=/var/lib/sysmon/state.json
EnvironmentFile=/etc/sysmon.env
ExecStart=/opt/sysmon/.venv/bin/python /opt/sysmon/sysmon.py
# /etc/systemd/system/sysmon.timer
[Unit]
Description=Run sysmon every minute

[Timer]
OnBootSec=1min
OnUnitActiveSec=1min

[Install]
WantedBy=timers.target
# Webhook の URL は root だけが読めるファイルに置く
echo 'MONITOR_WEBHOOK_URL=https://hooks.example.com/your-webhook-path' | sudo tee /etc/sysmon.env
sudo chmod 600 /etc/sysmon.env

sudo systemctl daemon-reload
sudo systemctl enable --now sysmon.timer
systemctl list-timers sysmon.timer      # 次回の実行時刻を確認
journalctl -u sysmon.service -n 20      # 直近の実行ログを確認

StateDirectory=sysmon を指定すると、systemd が /var/lib/sysmon を作り、User で指定したユーザーが書き込めるようにします。EnvironmentFile は systemd(root)が読み込んでから渡すので、ファイル自体の権限は 600 のままで構いません。

cron を使う場合は、* * * * * /opt/sysmon/.venv/bin/python /opt/sysmon/sysmon.py >> /var/log/sysmon.log 2>&1 のように、仮想環境の Python をフルパスで指定し、出力をログに残します。

動作確認:わざと閾値を超えさせる

設定しただけでは、通知が本当に届くかわかりません。一時的に閾値を低く(例: ディスク 1.0)して数分待ち、通知と回復の通知が1回ずつ届くことを確かめてから元に戻します。本番サーバーで負荷をかけて試すのは避け、閾値の側を動かして確認してください。

監視スクリプト自身の停止に気づく

自作の監視でいちばん見落とされやすいのは、スクリプトが止まっても誰も気づかないことです。Python の更新で仮想環境が壊れた、タイマーが無効になった、サーバーごと落ちた、という場合、通知は来ません。「通知が来ない=正常」とは限らないのです。

対策として、正常に動いたときに外部の監視サービスへ合図を送り、合図が途絶えたら通知してもらう方法(いわゆるデッドマンスイッチ)があります。Healthchecks.io のようなサービスが代表例です。あわせて、外部からの死活監視を1つ置いておくと、サーバー全体の停止にも気づけます。

監視する台数が増えたり、グラフで推移を見たくなったりしたら、Zabbix や Prometheus などの監視ツールへの移行を検討する時期です。自作スクリプトは、そうしたツールを入れるまでのつなぎ、または特定の確認を補う用途に向いています。

まとめ

  • psutil で CPU・メモリ(available 基準)・ディスクを取得する。cpu_percent は interval を指定する
  • 閾値超えが続いたときだけ通知し、通知済みの状態を記録して繰り返し通知を防ぐ
  • Webhook の URL は環境変数やアクセス権を絞ったファイルに置き、コードに書かない
  • systemd タイマーか cron で定期実行し、閾値を一時的に下げて通知を試す
  • スクリプト自身の停止は外部の監視で拾う

お気軽にご相談ください

お見積りへ お問い合わせへ
▲