はじめに
複数のクライアントサイトを保守していると、「サイトがダウンしていても気づくのが遅れた」という経験は一度はあるはずだ。理想はダウンを検知した瞬間に通知が来ること。
以前から Bash スクリプトで HTTP ステータスコードを定期チェックする死活監視を運用していた。しかし会社ごとに別スクリプトに分かれており、認証情報がコードに直書きされ、cron も2本走っている状態だった。
前回・前々回の記事で構築したディスク監視・SSL 証明書監視と同じサーバーに統合する機会に、全保守サイトを一括で監視する PHP システムに書き直した。今回はその実装を紹介する。
旧 Bash スクリプトの問題点
旧システムは monitor.sh という Bash スクリプトで、会社名を引数に渡して実行する形だった。
/bin/bash monitor.sh group-a # グループA担当サイトをチェック
/bin/bash monitor.sh group-b # グループB担当サイトをチェック
config_group-a.sh と config_group-b.sh の2ファイルに設定が分かれており、Chatwork の API トークンや通知先ルーム ID がそれぞれ直書きされていた。cron も2本で、毎時5分と毎時15分にずらして実行していた。
主な問題を整理すると以下のとおり。
| 問題点 | 内容 |
|---|---|
| 設定の分散 | 会社別に2ファイル・2 cron |
| 認証情報の直書き | API トークンがスクリプト内に記述 |
| 通知ロジックの複雑さ | 時間帯制限(08:00〜22:00)を Bash で実装 |
| サマリーが毎時 | 正常時でも毎時通知が届く |
「時間帯制限」は「夜中に通知で起こされたくない」という要件だったが、cron スケジュールで制御するほうがシンプルだと判断し、PHP 版では廃止した。
設計方針
新システムの設計で決めたことは以下の3点。
- 全サイトを1つのスクリプトで管理。会社別の分割をやめ、
config.phpの配列に全 URL を列挙する - 通知を2つのフェーズに分ける。毎時チェック(異常時のみアラート)と日次サマリー(常に送信)を別スクリプトに分離する
- 通知グループで送信先を制御。どの URL の通知を誰に送るかを
notification_groupsで設定し、スクリプト本体は変更不要にする
実装
ファイル構成
ディスク監視・SSL 証明書監視と同じ monitor/ ディレクトリ以下に配置し、共有ライブラリを使う構成にした。
manage/
├── lib/ ← 共有ライブラリ(全モジュール共用)
│ ├── Env.php # .env ローダー
│ └── Notifier.php # 通知クラス(メール / Chatwork / Slack / Discord)
└── site_monitor/
├── .env # 認証情報(Git 管理外)
├── config.php # 監視サイト・チェック設定・通知グループ
├── site_check.php # 毎時実行:異常時のみアラート
├── site_report.php # 日次実行:全サイトサマリー
├── deploy.sh # デプロイスクリプト
└── lib/
├── SiteChecker.php # HTTP チェック・リトライ
└── SiteCsvLogger.php # CSV 記録
SiteChecker — curl でチェック・リトライあり
HTTP ステータスコードを curl で取得するシンプルなクラス。OK とみなすのは 200・301・302 の3つ。
class SiteChecker
{
private const OK_CODES = [200, 301, 302];
public function __construct(
private int $timeout = 30,
private int $retryCount = 2,
private int $retryInterval = 5
) {}
public function check(string $url): array
{
$code = $this->fetch($url);
if ($this->isOk($code)) return $this->result($url, $code, 'ok');
for ($i = 0; $i < $this->retryCount; $i++) {
sleep($this->retryInterval);
$code = $this->fetch($url);
if ($this->isOk($code)) return $this->result($url, $code, 'ok');
}
return $this->result($url, $code, $code === 0 ? 'error' : 'down');
}
private function fetch(string $url): int
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false, // リダイレクト先はチェックしない
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_USERAGENT => 'SiteMonitor/1.0',
CURLOPT_SSL_VERIFYPEER => false, // SSL エラーは ssl_monitor に委任
CURLOPT_SSL_VERIFYHOST => false,
]);
curl_exec($ch);
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $code;
}
}
CURLOPT_FOLLOWLOCATION => false にしている点がポイントだ。リダイレクト設定の問題でループが発生したり、最終的な転送先のステータスコードと元の URL のステータスが混在したりするケースを避けるため、301・302 はそのまま OK と判定している。
一時的なネットワーク障害でアラートが誤発砲しないよう、失敗時は5秒待って2回リトライする。3回連続で失敗した場合のみ異常と判断する。
ステータスの定義は以下。
| status | 意味 |
|---|---|
ok | HTTP 200 / 301 / 302 |
down | その他の HTTP コード(4xx / 5xx など) |
error | curl 接続失敗(タイムアウト・DNS 解決失敗など) |
SiteCsvLogger — サイトごとに日別 CSV で保存
チェック結果を CSV ファイルに追記する。ファイル名はホスト名+パスのスラグと日付を組み合わせ、サイトごと・日付ごとにファイルを分割する。
data/
├── example-a_com_20260424.csv
├── example-b_jp_20260424.csv
└── example-b_jp_navi_20260424.csv ← パスが含まれる URL はパスも含む
CSV の各行。
datetime,url,label,status_code,status
"2026-04-24 09:05:00",https://example-a.com/,サンプルサイトA,200,ok
"2026-04-24 09:05:00",https://example-b.jp/,サンプルサイトB,503,down
config.php — 監視対象と通知グループ
'sites' => [
['url' => 'https://example-a.com/', 'label' => 'サンプルサイトA'],
['url' => 'https://example-b.jp/', 'label' => 'サンプルサイトB'],
['url' => 'https://example-b.jp/navi/', 'label' => 'サンプルサイトB ナビ'],
// ...
],
'checker' => [
'timeout' => 30,
'retry_count' => 2,
'retry_interval' => 5,
],
通知グループ:送信先・チャンネルの分離
旧 Bash では会社ごとに Chatwork ルームが異なっていた。PHP 版でもこの構成を維持し、notification_groups で管理する。
さらに、日次サマリー用チャンネル(channels)と異常アラート用チャンネル(alert_channels)を同じグループ内で分けた。旧 Bash では通常通知ルームとアラートルームが別だったためだ。
'notification_groups' => [
[
'label' => 'グループA',
'urls' => [
'https://example-a.com/',
'https://example-a.jp/',
],
'channels' => [ // site_report.php(日次サマリー)で使用
'chatwork' => ['enabled' => true, 'room_id' => Env::get('NOTIFY_A_CHATWORK_ROOM_ID')],
'slack' => ['enabled' => true, 'webhook_url' => Env::get('NOTIFY_A_SLACK_WEBHOOK_URL')],
'email' => ['enabled' => true, 'to' => 'summary@example.com', ...],
],
'alert_channels' => [ // site_check.php(毎時アラート)で使用
'chatwork' => ['enabled' => true, 'room_id' => Env::get('NOTIFY_A_ALERT_CHATWORK_ROOM_ID')],
'slack' => ['enabled' => true, 'webhook_url' => Env::get('NOTIFY_A_SLACK_WEBHOOK_URL')],
'email' => ['enabled' => true, 'to' => 'alert@example.com', ...],
],
],
[
'label' => 'グループB',
'urls' => [
'https://example-b.com/',
// ...
],
'channels' => [ /* グループB のサマリー通知先 */ ],
'alert_channels' => [ /* グループB のアラート通知先 */ ],
],
],
グループを追加・変更しても config.php だけを修正すればよく、スクリプト本体には触れない。
site_check.php — 毎時チェック・異常時のみ通知
全サイトをチェックし、down または error のサイトが1件でもあれば alert_channels へ送信する。全サイト正常の場合はサイレント。
foreach ($config['notification_groups'] as $group) {
$issues = [];
foreach ($results as $url => $data) {
if (!urlInGroup($url, $group['urls'])) continue;
if (in_array($data['result']['status'], ['down', 'error'])) {
$issues[$url] = $data;
}
}
if (empty($issues)) continue; // 正常ならスキップ
// alert_channels がなければ channels にフォールバック
$channels = $group['alert_channels'] ?? $group['channels'];
(new Notifier($channels))->send('【サイト監視異常通知】異常が検知されました', buildAlertBody($issues));
}
異常時の通知フォーマットは旧 Bash に合わせた。
件名: 【サイト監視異常通知】異常が検知されました
【サイト監視異常通知】
日時: 2026-04-24 09:05:00
異常検知されたサイト:
https://example-b.jp/
状態: ダウンしています
ステータスコード: 503
site_report.php — 日次サマリー・常に送信
毎朝9時に全サイトのチェックを実行し、結果を channels へ送信する。問題の有無に関わらず常に送信するため、「問題がないこと」の確認にもなる。ステータスの深刻度順(down → error → ok)でソートし、問題のあるサイトが先頭に並ぶ。
件名: 【サイト監視サマリー】全サイトの状態レポート
【サイト監視結果サマリー】
日時: 2026-04-24 09:05:00
監視結果:
https://example-b.jp/
状態: ダウンしています
ステータスコード: 503
![]()
EXAMPLE-AEXAMPLE-A is a dynamic web application crafted with Next.js, a React-based framework, designed to provide users with eff...
状態: 正常に稼働中
ステータスコード: 200
![]()
https://example-a.jp/
状態: 正常に稼働中
ステータスコード: 301
異常があった場合は件名に *** 異常あり *** が付く。
旧 Bash との比較
| 項目 | 旧 Bash | 新 PHP |
|---|---|---|
| スクリプト数 | 会社別 2 スクリプト | 全サイト共通 1 セット |
| cron 数 | 2本(毎時5分・毎時15分) | 2本(毎時5分・毎朝9時) |
| 認証情報の管理 | コード直書き | .env ファイル |
| サマリー送信 | 毎時(時間帯制限あり) | 日次(cron スケジュールで制御) |
| 通知チャンネル | メール・Chatwork・Slack | メール・Chatwork・Slack(同等) |
| 通知グループ | 会社ごとに別スクリプト | notification_groups で一元管理 |
| サイト追加 | 2ファイルを修正 | config.php の1配列に追記 |
cron が2本のままなのは変わらないが、役割が「毎時チェック(アラート)」と「日次サマリー」に整理されたことで、通知が届いたときの意味が明確になった。
デプロイと cron 登録
デプロイは rsync ベースの deploy.sh で行う。.env・data/・ログは転送対象から除外され、サーバー側のファイルが保持される。
# 転送内容の確認
./deploy.sh
# 本番デプロイ
./deploy.sh --deploy
デプロイ後、リモートで PHP の構文チェックが自動実行される。
cron は XServer API で登録する。
# 毎時5分:死活チェック(異常時のみ通知)
curl -s -X POST \
-H "Authorization: Bearer xs_xxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"minute": "5", "hour": "*", "day": "*", "month": "*", "weekday": "*",
"command": "/opt/php-8.3/bin/php $HOME/shell/monitor/site_monitor/site_check.php",
"comment": "サイト死活監視:毎時チェック(毎時5分)"
}' \
"https://api.xserver.ne.jp/v1/server/your-server.xsrv.jp/cron"
# 毎朝9時:日次サマリー(常に送信)
curl -s -X POST \
-H "Authorization: Bearer xs_xxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"minute": "0", "hour": "9", "day": "*", "month": "*", "weekday": "*",
"command": "/opt/php-8.3/bin/php $HOME/shell/monitor/site_monitor/site_report.php",
"comment": "サイト死活監視:日次レポート(毎朝9時)"
}' \
"https://api.xserver.ne.jp/v1/server/your-server.xsrv.jp/cron"
旧 Bash の cron を削除する際も同じ API の DELETE エンドポイントを使えばよい。cron ID は GET で一覧取得できる。
まとめ
今回構築したシステムの要点を整理する。
| 項目 | 採用した方法 |
|---|---|
| HTTP チェック方式 | curl(CURLOPT_FOLLOWLOCATION => false) |
| OK 判定 | HTTP 200 / 301 / 302 |
| 障害の誤検知対策 | 5秒待ちリトライ × 2回 |
| SSL エラーの扱い | SiteChecker では無視(ssl_monitor に委任) |
| ログ形式 | サイトごと・日付ごとの CSV ファイル |
| 通知フェーズ | 毎時アラート(異常時のみ)+日次サマリー(常時) |
| 通知グループ | channels(サマリー用)と alert_channels(アラート用)を分離 |
| 共有ライブラリ | Env.php / Notifier.php をディスク監視・SSL 監視と共用 |
| cron 管理 | XServer API 経由で登録・削除 |
ディスク監視・SSL 証明書監視と合わせて、3つの監視モジュールが /home/shell/monitor/ 以下に統合された。共有ライブラリを更新するとすべてのモジュールに反映されるため、通知ロジックの改善も1か所で済む。
shell/monitor/
├── lib/ ← Env.php / Notifier.php(3モジュール共用)
├── xserver_monitor/ ← ディスク使用量監視
├── ssl_monitor/ ← SSL 証明書監視
└── site_monitor/ ← サイト死活監視(今回)
サイトを追加する際は config.php の sites 配列に1行追記してデプロイするだけ。特定のグループにも通知したい場合は notification_groups の urls に追加する。運用コストを最小限に保ちながらカバレッジを広げていける構成になった。


コメント