サーバー監視をPythonスクリプトで自作してみた話

VPSでWordPressを動かし始めてから、「サーバーの状態を常に把握したい」と思うようになった。

DatadogやNew Relicは高機能だが、個人のVPS一台のために月額料金を払うのは気が引ける。Zabbixは高機能すぎて構築が大変。Prometheus + Grafanaも同様だ。

だったら、必要な監視だけPythonで自作してSlackに通知すればいい。そう決めて作ったのが今回紹介するスクリプトだ。

監視したい項目

シンプルに3点に絞った。

1. CPU使用率: 80%以上が5分続いたらアラート

2. メモリ使用率: 85%以上でアラート

3. ディスク使用率: 90%以上でアラート

これだけでも、プロセスが暴走していたり、ディスクが満杯でWordPressが壊れたりするのを事前に検知できる。

依存パッケージ

pip install psutil requests python-dotenv
  • `psutil`: CPU/メモリ/ディスク情報を取得するライブラリ
  • `requests`: Slack Webhook呼び出し用
  • `python-dotenv`: 環境変数管理
  • スクリプト全体

    #!/usr/bin/env python3
    """
    server_monitor.py - サーバー基本監視 + Slack通知
    """
    
    import os
    import time
    import logging
    from datetime import datetime
    from typing import Optional
    
    import psutil
    import requests
    from dotenv import load_dotenv
    
    load_dotenv()
    
    # 設定
    SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']
    CHECK_INTERVAL_SEC = 60          # チェック間隔(秒)
    CPU_THRESHOLD = 80.0             # CPU警告しきい値(%)
    MEMORY_THRESHOLD = 85.0          # メモリ警告しきい値(%)
    DISK_THRESHOLD = 90.0            # ディスク警告しきい値(%)
    CPU_SUSTAINED_COUNT = 5          # CPU高負荷が何回連続でアラートを出すか
    
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s [%(levelname)s] %(message)s'
    )
    logger = logging.getLogger(__name__)
    
    
    def get_cpu_usage() -> float:
        """CPU使用率(%)を返す。interval=1でより正確な値を取得。"""
        return psutil.cpu_percent(interval=1)
    
    
    def get_memory_usage() -> dict:
        """メモリ使用状況を返す。"""
        mem = psutil.virtual_memory()
        return {
            'total_gb': mem.total / (1024 ** 3),
            'used_gb': mem.used / (1024 ** 3),
            'percent': mem.percent,
        }
    
    
    def get_disk_usage(path: str = '/') -> dict:
        """ディスク使用状況を返す。"""
        disk = psutil.disk_usage(path)
        return {
            'total_gb': disk.total / (1024 ** 3),
            'used_gb': disk.used / (1024 ** 3),
            'percent': disk.percent,
        }
    
    
    def send_slack_alert(message: str, level: str = 'warning') -> bool:
        """Slack Webhookにアラートを送信する。"""
        emoji = {'warning': ':warning:', 'critical': ':rotating_light:', 'ok': ':white_check_mark:'}
        color = {'warning': '#FFA500', 'critical': '#FF0000', 'ok': '#36a64f'}
    
        payload = {
            'attachments': [{
                'color': color.get(level, '#FFA500'),
                'text': f"{emoji.get(level, '')} {message}",
                'footer': f"server-monitor | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
            }]
        }
    
        try:
            res = requests.post(SLACK_WEBHOOK_URL, json=payload, timeout=10)
            res.raise_for_status()
            return True
        except requests.RequestException as e:
            logger.error(f"Slack通知失敗: {e}")
            return False
    
    
    def format_alert_message(metric: str, value: float, threshold: float, unit: str = '%') -> str:
        return f"[{metric}] 現在値: {value:.1f}{unit} (しきい値: {threshold:.0f}{unit})"
    
    
    def monitor_loop():
        """監視ループのメインロジック。"""
        cpu_high_count = 0
        last_disk_alert_time: Optional[float] = None
        last_memory_alert_time: Optional[float] = None
        alert_cooldown_sec = 1800  # 同じアラートを30分以内に再送しない
    
        logger.info("サーバー監視を開始します")
        send_slack_alert("サーバー監視を開始しました", level='ok')
    
        while True:
            now = time.time()
    
            # CPU監視
            cpu = get_cpu_usage()
            if cpu >= CPU_THRESHOLD:
                cpu_high_count += 1
                logger.warning(f"CPU高負荷: {cpu:.1f}% ({cpu_high_count}回連続)")
                if cpu_high_count >= CPU_SUSTAINED_COUNT:
                    send_slack_alert(
                        format_alert_message('CPU使用率', cpu, CPU_THRESHOLD),
                        level='critical'
                    )
                    cpu_high_count = 0  # アラート送信後リセット
            else:
                if cpu_high_count > 0:
                    logger.info(f"CPU正常に戻りました: {cpu:.1f}%")
                cpu_high_count = 0
    
            # メモリ監視
            mem = get_memory_usage()
            if mem['percent'] >= MEMORY_THRESHOLD:
                if last_memory_alert_time is None or (now - last_memory_alert_time) > alert_cooldown_sec:
                    msg = (
                        format_alert_message('メモリ使用率', mem['percent'], MEMORY_THRESHOLD)
                        + f"\n使用量: {mem['used_gb']:.1f}GB / {mem['total_gb']:.1f}GB"
                    )
                    send_slack_alert(msg, level='warning')
                    last_memory_alert_time = now
                    logger.warning(f"メモリ高負荷: {mem['percent']:.1f}%")
    
            # ディスク監視
            disk = get_disk_usage('/')
            if disk['percent'] >= DISK_THRESHOLD:
                if last_disk_alert_time is None or (now - last_disk_alert_time) > alert_cooldown_sec:
                    msg = (
                        format_alert_message('ディスク使用率', disk['percent'], DISK_THRESHOLD)
                        + f"\n使用量: {disk['used_gb']:.1f}GB / {disk['total_gb']:.1f}GB"
                    )
                    send_slack_alert(msg, level='critical')
                    last_disk_alert_time = now
                    logger.warning(f"ディスク残量少: {disk['percent']:.1f}%")
    
            logger.info(
                f"CPU: {cpu:.1f}% | "
                f"Memory: {mem['percent']:.1f}% | "
                f"Disk: {disk['percent']:.1f}%"
            )
            time.sleep(CHECK_INTERVAL_SEC)
    
    
    if __name__ == '__main__':
        monitor_loop()

    環境変数の設定

    # .env
    SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXX/YYY/ZZZ

    Slack Webhookは、SlackのApp設定で「Incoming Webhooks」を有効にして取得する。

    systemdで常駐化

    スクリプトを常時起動し、サーバー再起動後も自動で立ち上がるようにする。

    systemdのサービスファイル(server-monitor.service)を以下の内容で作成する:

    [Unit]
    Description=Server Monitor
    After=network.target
    
    [Service]
    Type=simple
    User=nakano
    WorkingDirectory=/home/nakano/server-monitor
    ExecStart=/home/nakano/server-monitor/venv/bin/python server_monitor.py
    Restart=always
    RestartSec=10
    EnvironmentFile=/home/nakano/server-monitor/.env
    
    [Install]
    WantedBy=multi-user.target
    $ sudo systemctl daemon-reload
    $ sudo systemctl enable server-monitor
    $ sudo systemctl start server-monitor
    $ sudo systemctl status server-monitor

    運用してみた感想

    よかった点

    本当に必要なアラートだけが来る。DatadogのデフォルトアラートはNoisy(うるさい)で、重要でない通知も大量に来る。自作だと完全にコントロールできる。

    ディスク満杯を事前に検知できた。WordPressのアクセスログが膨れ上がりかけていたのをアラートで気づき、ログローテーションを設定した。監視がなければ確実に気づかなかった。

    課題

    プロセス監視がない。NginxやPHP-FPMが死んでいてもこのスクリプトでは検知できない。psutil.process_iter() でプロセスの存在確認を追加するのが次のステップだ。

    HTTPレスポンス監視がない。サーバー自体は生きていてもWordPressが503を返しているケースは、外からHTTPリクエストを飛ばして確認する必要がある。

    まとめ

    psutil + requests + systemdの組み合わせで、個人VPS向けのシンプルな監視システムを自作できた。

    高機能な監視ツールは不要で、「異常があればSlackに通知」というだけであればPython 50行程度で十分だ。自分が必要な監視項目だけに絞ることで、アラートのノイズが少なく実用的な監視ができている。