XServer API ディスク監視システム拡張編:通知グループ・SMTP 認証・データローテーション

はじめに

前回の記事(XServer API で複数サーバーのディスク使用量を集中監視する)では、エックスサーバーの REST API(XServer API)を使って複数サーバーのディスク使用量を PHP で集中監視する仕組みを構築した。

その後、実運用を重ねる中で次の3つの課題が出てきた。

  1. 通知先の制御 — 運営者は全サーバーを把握したいが、クライアントには担当サーバーの情報だけを届けたい
  2. メール送信の信頼性 — 共有ホスティングの mail() 関数ではなく SMTP 認証で確実に送りたい
  3. ログファイルの肥大化 — 毎時追記し続けると CSV が際限なく大きくなる

この記事ではこれら3つを解決するために追加した機能を紹介する。


通知グループ:受信者別のサーバー範囲制御

課題

監視対象サーバーが増えると、受け取りたい通知の範囲が受信者によって異なってくる。

  • 保守サービス運営者 → 全サーバーのアラートとレポートを受け取りたい
  • サービス利用企業(複数サイト担当) → 担当する複数サーバーの通知のみ
  • 1サイトのみ管理の担当者 → そのサーバーの通知だけ

以前の実装は notification という単一の通知設定に全サーバーのアラートがまとめて送られていた。クライアントごとに通知先を分けるには設計の変更が必要だった。

設計:通知グループ配列

config.phpnotification_groups 配列を追加し、グループごとに「対象サーバーの範囲」と「通知チャネル」を独立して定義する方式にした。

'notification_groups' => [
    // 運営者:全サーバーを対象
    [
        'label'   => '運営者(全体)',
        'servers' => '*',
        'channels' => [
            'email' => ['enabled' => true, 'to' => 'admin@example.com', ...],
        ],
    ],
    // クライアントA:担当サーバーのみ
    [
        'label'   => 'クライアントA',
        'servers' => ['server-b.xsrv.jp'],
        'channels' => [
            'email' => ['enabled' => true, 'to' => 'client-a@example.com', ...],
        ],
    ],
],

servers の指定は次の3パターン。

対象用途例
'*'全監視対象保守サービス運営者
['server-a.xsrv.jp', 'server-b.xsrv.jp']指定した複数サーバー複数サイト担当クライアント
['server-c.xsrv.jp']指定した1サーバー1サイトのみの担当者

各グループは独立したチャネル設定を持つため、グループAはメール、グループBは Slack、といった組み合わせも可能だ。

実装:収集してからフィルタして送信

スクリプト(disk_check.phpdisk_report.php)の処理を2フェーズに分けた。

フェーズ1:全サーバーのデータを収集

$results = [];  // [servername => ['label', 'alert_msg', 'error']]

foreach ($config['servers'] as $server) {
    try {
        $api   = new XServerApi($server['servername'], $server['api_key']);
        $usage = $api->getUsage();
        $row   = $logger->append($server['servername'], $usage);

        $alertMsg = ((float)$row['usage_pct'] >= $server['thresholds']['disk_usage_pct'])
            ? sprintf('[%s] 使用率 %s%% ※閾値超過', $server['label'], $row['usage_pct'])
            : null;

        $results[$server['servername']] = ['label' => $server['label'], 'alert_msg' => $alertMsg, 'error' => null];
    } catch (Exception $e) {
        $results[$server['servername']] = ['label' => $server['label'], 'alert_msg' => null, 'error' => $e->getMessage()];
    }
}

フェーズ2:グループごとにフィルタして送信

foreach ($config['notification_groups'] as $group) {
    $alerts = [];
    $errors = [];

    foreach ($results as $sname => $result) {
        if (!serverInGroup($sname, $group['servers'])) continue;
        if ($result['error'])     $errors[] = $result['error'];
        if ($result['alert_msg']) $alerts[] = $result['alert_msg'];
    }

    if (empty($alerts) && empty($errors)) continue; // このグループは正常

    (new Notifier($group['channels']))->send('[XServer監視] アラート', implode("\n", $alerts));
}

function serverInGroup(string $servername, string|array $servers): bool
{
    return $servers === '*' || (is_array($servers) && in_array($servername, $servers, true));
}

スクリプト側はグループが何件あっても変更不要で、グループの追加・削除は config.php だけで完結する。


メール送信方式の切り替え:mail() と SMTP 認証

課題

PHP の mail() 関数は共有ホスティングでも手軽に使えるが、送信元 IP や SPF 設定によっては迷惑メールフォルダに入ることがある。Gmail や自社の SMTP サーバーを経由して送信したい場面に対応するため、SMTP 認証での送信機能を追加した。

設計:driver による切り替え

config.phpemail 設定に driver キーを追加し、値を変えるだけで切り替えられるようにした。

'email' => [
    'enabled' => true,
    'driver'  => 'smtp',   // 'mail'(デフォルト)または 'smtp'
    'to'      => 'admin@example.com',
    'from'    => 'monitor@your-server.xsrv.jp',
    'smtp' => [
        'host'       => 'smtp.gmail.com',
        'port'       => 587,
        'encryption' => 'tls',   // 'tls'(STARTTLS)/ 'ssl'(SMTPS)/ 'none'
        'username'   => 'your@gmail.com',
        'password'   => 'app-password',  // .env で管理
    ],
],

Notifier::sendEmail() がドライバーを判定して sendEmailMail()sendEmailSmtp() を呼び分ける。公開インターフェースは変わらないため、呼び出し側のコードに変更はない。

実装:Composer 不要の socket ベース SMTP

外部ライブラリを使わず、PHP の stream_socket_client で直接 SMTP プロトコルを実装した。共有ホスティング環境でも Composer なしで動作する。

private function sendEmailSmtp(string $subject, string $body): void
{
    $cfg = $this->config['email'];
    $enc = $cfg['smtp']['encryption'] ?? 'tls';

    // ssl: 最初から TLS 接続(SMTPS)、tls/none: 平文で接続
    $uri  = ($enc === 'ssl') ? "ssl://{$cfg['smtp']['host']}:{$cfg['smtp']['port']}"
                             : "tcp://{$cfg['smtp']['host']}:{$cfg['smtp']['port']}";
    $sock = stream_socket_client($uri, $errno, $errstr, 30);

    // ... 接続バナー確認 → EHLO 送信 ...

    // STARTTLS の場合:平文接続後に TLS へアップグレード
    if ($enc === 'tls') {
        // STARTTLS コマンドを送信
        stream_socket_enable_crypto($sock, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
        // TLS 確立後に EHLO を再送
    }

    // AUTH LOGIN で認証
    // MAIL FROM / RCPT TO / DATA でメール送信
}

encryption の指定方法は次の通り。

ポート動作
'tls'587平文で接続後 STARTTLS で暗号化
'ssl'465最初から SSL/TLS 接続(SMTPS)
'none'任意暗号化なし(社内リレー等)

SMTP 認証情報は .env で管理し、config.php では Env::get('SMTP_PASSWORD') として参照するため、ソースコードにパスワードは含まれない。


CSV ログの保持とローテーション

課題

毎時1行ずつ追記し続けると、3サーバーで年間約26,000行・約2.4MB になる。数年運用すると無視できないサイズになり、またバックアップや分析の単位としても扱いにくい。

「古いデータを自動削除してファイルを小さく保ちたい」という要件と、「過去のデータも月別にアーカイブして残したい」という要件が同時に存在したため、2つの独立した機能として実装した。

設計:retention と rotation を分離

'data_retention_days' => 90,     // アクティブファイルの保持日数(0 = 無制限)

'data_rotation' => [
    'enabled' => false,
    'period'  => 'monthly',      // 'daily', 'weekly', 'monthly', 'yearly'
    'keep'    => 12,             // 保持するローテーションファイル数(0 = 無制限)
],
  • retention: {servername}.csv(アクティブファイル)から N日以上前の行を削除
  • rotation: append() のたびに {servername}-2026-04.csv のような期間別ファイルへも同時書き込み

両方を有効にすると次のような構成になる。

data/
├── your-server.xsrv.jp.csv          ← アクティブ(直近90日・古い行は自動削除)
├── your-server.xsrv.jp-2026-03.csv  ← 3月の全データ(ローテーション)
└── your-server.xsrv.jp-2026-04.csv  ← 4月の全データ(書き込み中)

アクティブファイルで直近データをすばやく参照しながら、月別ファイルで長期トレンドを分析できる。

実装:必要なときだけ rewrite する retention

毎回ファイル全体を書き直すのは無駄が大きい。先頭行(最古)をチェックして期限切れの行があるときだけ rewrite する設計にした。

private function pruneOldRows(string $file): void
{
    $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    if (count($lines) < 2) return;

    $cutoff    = date('Y-m-d H:i:s', strtotime("-{$this->retentionDays} days"));
    $firstData = str_getcsv($lines[1]);

    // 先頭データ行が期限内 → 削除不要(rewrite しない)
    if (($firstData[0] ?? '') >= $cutoff) return;

    $header   = $lines[0];
    $retained = array_filter(
        array_slice($lines, 1),
        fn($line) => (str_getcsv($line)[0] ?? '') >= $cutoff
    );

    $fh = fopen($file, 'w');
    fwrite($fh, $header . "\n");
    foreach ($retained as $line) { fwrite($fh, $line . "\n"); }
    fclose($fh);
}

rewrite が発生するのは最古行が保持期間を超えた瞬間だけなので、通常の実行ではオーバーヘッドはほぼゼロだ。

実装:ファクトリメソッドによる設定の集約

disk_check.phpdisk_report.php の両方で CsvLogger を生成する処理が必要になるため、fromConfig() ファクトリメソッドを追加して config 配列から直接インスタンスを生成できるようにした。

// CsvLogger クラス
public static function fromConfig(array $config): self
{
    $rot    = $config['data_rotation'] ?? [];
    $period = !empty($rot['enabled']) ? ($rot['period'] ?? 'monthly') : '';

    return new self(
        $config['data_dir'],
        $config['data_retention_days'] ?? 0,
        $period,
        (int)($rot['keep'] ?? 0)
    );
}

// 各スクリプト(変更前:3行 → 変更後:1行)
$logger = CsvLogger::fromConfig($config);

まとめ

今回追加した機能を整理する。

機能実装内容
通知グループnotification_groups で受信者ごとにサーバー範囲と通知先を独立設定
メール送信方式driver: 'mail' / 'smtp' で切り替え。SMTP は Composer 不要の socket 実装
データ保持(retention)アクティブ CSV から N日以上前の行を自動削除。変化があるときのみ rewrite
データローテーション期間別 CSV に同時書き込み(月次など)。古いファイルを自動削除

いずれの機能も config.php の設定を変えるだけで有効化・無効化でき、スクリプト本体を変更する必要はない。設定ファイルに権限を集中させることで、運用中の変更ミスを減らせる。

XServer API(エックスサーバーの REST API)はサーバー・ドメイン・メール・cron など広い範囲をカバーしている。今後は SSL 証明書の期限監視や JSON エクスポートによる他システム連携なども検討している。

コメント

タイトルとURLをコピーしました