RHEL系Linux環境でsystemdユニットのjournaldエラーログを常時監視し、差分検知でLINEやメールに自動通知する方法を解説します。
多重起動防止、PID管理、除外パターン設定など実用運用に必須の設計・実装・運用例を網羅します。
自動化運用ツール
🟡 自動化運用ツール
📌 面倒な作業を一括効率化!シェルスクリプトで実現する運用自動化術
├─ 基本共通
| ├─【Shellの基礎知識】簡単なログ出力ロジックを作ってみました。
| ├─【Shellの基礎知識】シェルスクリプトの作成を時短!テンプレートで効率化する方法
| ├─【Shellの基礎知識】共通関数定義クラスの完全ガイド!設計から実践まで徹底解説
├─ 開発環境構築
| ├─【RHEL系Linux】開発サーバー初期設定スクリプトの完全自動化
| ├─【RHEL系Linux】Apache+Let's Encrypt 自動構築スクリプト|バーチャルホスト対応
| ├─【RHEL系Linux】Tomcatを自動インストール・設定するスクリプトの作成と活用法
| └─【RHEL系Linux】PostgreSQLを自動インストールするシェルスクリプトの使い方
└─ 運用・保守
├─【RHEL系Linux】任意サービスを簡単制御!汎用サービススクリプトの活用術
├─【RHEL系Linux】ファイルやログを自動圧縮する汎用スクリプトの実装と活用法
├─【RHEL系Linux】リソース(CPU・MEM)監視スクリプトで使用率・異常を検知する仕組み
├─【RHEL系Linux】中間ファイル連携を完全制御するファイル転送スクリプト
├─【RHEL系Linux】信頼性を重視した完了保証型ディレクトリ転送スクリプトの設計と実装
├─【RHEL系Linux】ディスク使用率を自動監視するシェルスクリプトの実装
└─【RHEL系Linux】サーバーの障害検知と自動通知|systemdログ監視の実装例
概要
Linuxサーバーの安定稼働を維持するには、障害発生時にいち早く検知し、迅速に対応する体制が必要です。本記事では、systemdのjournaldログを監視し、エラーログや警告をリアルタイムに検知してLINEまたはメールで通知する仕組みを構築します。
また、多重起動防止やPID管理、除外パターン設定など、運用環境にそのまま適用できる実装例を紹介します。
目的と背景
サーバー障害は、サービス停止やデータ損失など重大な影響を引き起こす可能性があります。特にデータベースやWebアプリケーションなど、常時稼働が求められるプロセスは、障害発生から復旧までの時間がビジネス継続に直結します。手動監視や定期的なログ確認では対応が遅れるため、systemdのjournaldログを自動で監視し、即時通知することで、復旧時間を短縮することを目的とします。
想定環境と前提条件
本記事で扱う構成は、以下の環境を前提としています。
項目 | 内容 |
---|---|
OS | RHEL系Linux(RHEL、CentOS、Rocky Linuxなど) |
監視対象 | systemd管理下のサービス(例:postgresql、Tomcat、Apache)など |
通知方法 | LINE Messaging APIまたはメール |
権限 | root権限での実行 |
依存スクリプト | logger.shrc、utils.shrc |
適用範囲と制限事項
この監視スクリプトは、systemdによって管理されているサービスのjournaldログ監視に特化しています。そのため、systemd非対応のサービスや、外部のクラウド監視サービスとは直接連携できません。
また、通知方法はLINE Messaging APIとメール送信に限定しており、SlackやTeamsなど他のチャットツールへは標準では対応していません。除外パターンの設定や監視間隔の調整は可能ですが、監視対象が大量の場合は負荷が高くなる可能性があります。
設計・仕様
本章では、systemdのjournaldログを監視し、障害検知時にLINEまたはメールで通知する監視スクリプトの設計と仕様について説明します。ディレクトリ構成や引数仕様、通知の仕組みなど、運用時に必要となる要素を整理します。
ディレクトリ構成と配置ルール
スクリプトおよび関連ファイルは、以下のディレクトリ構成で配置します。この構成により、運用時の保守性と可読性を確保します。
ディレクトリ | 用途 |
---|---|
scripts/bin | 実行スクリプト(send_alert.shなど)を配置 |
scripts/com | 共通関数・ライブラリ(logger.shrc、utils.shrcなど)を配置 |
scripts/etc | 設定ファイルや除外パターンファイルを配置 |
scripts/log | スクリプトの実行ログを保存 |
scripts/tmp | PIDファイルやロックディレクトリなど一時ファイルを保存 |
引数とオプション仕様
監視スクリプトは複数のモードとオプションを持ち、用途に応じた実行が可能です。
オプション | 説明 | 必須 |
---|---|---|
-m | 実行モード(start、stop、status、once、list) | 必須 |
-u | 監視対象のsystemdユニット名(例:postgresql-15) | 必須(list以外) |
-t | 通知先(lineまたはmail、デフォルトはline) | 任意 |
-i | 監視間隔(秒、デフォルト60秒) | 任意(runモードで有効) |
環境変数と設定ファイル構造
環境変数はホストごとに定義された.envファイルで管理します。このファイルにはLINEのAPIキーやメール送信先などの機密情報を格納します。
変数名 | 用途 |
---|---|
LINE_CHANNEL_ACCESS_TOKEN | LINE Messaging APIのアクセストークン |
MAIL_TO | 通知メールの送信先アドレス |
.env設定例
このファイルはホスト名ごとに etc/<ホスト名>/.envとして配置します。機密情報を含むため、アクセス権は600に設定してください。
LINE_CHANNEL_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
MAIL_TO=admin@example.com
必要な場合は、Messaging APIのWebhook受信や署名検証機能を実装した際に追加してください。
ロック機構とPID管理仕様
多重起動を防止するために、監視開始時に専用のロックディレクトリとPIDファイルを生成します。PIDファイルには実行中のプロセスIDを記録し、監視停止時に必ず削除します。ロックディレクトリの形式は「<ユニット名>.lock」とし、scripts/tmp配下に配置します。
これにより、同じユニット名での監視スクリプトの二重起動を防ぎ、異常終了時にも状態を確認できます。また、異常終了やシステム再起動後でもロックディレクトリとPIDファイルを参照することで、監視状態の復旧や調査が容易になります。
ログ出力仕様(logger.shrc準拠)
ログはすべてlogger.shrcを経由して出力します。ログレベルはINFO、DEBUG、ERROR、WARNなどを用意し、以下の形式で出力します。
logOut "INFO" "監視プロセスを起動しました"
これにより、全スクリプトで統一されたログ形式を維持し、運用時の解析を容易にします。
通知仕様(LINE・メール)
障害検知時の通知はLINE Messaging APIまたはmailコマンドで行います。LINEはAPIキーを用いたJSON送信で、スマートフォンなどに即時通知されるため、迅速な対応が可能です。
メール通知はシステムに標準搭載されたmailコマンドを利用しますが、今回はメールサーバーを構築するとセキュリティ設定(認証方式、暗号化設定、外部送信制限など)まで踏み込む必要があるため、運用の初期段階ではLINE通知を優先する方法を採用します。
これにより、障害発生時の検知速度を確保しつつ、構築・運用負荷を軽減します。
監視パターンと除外設定
監視対象のログは、journalctlコマンドで取得したsystemdユニットログとloggerタグ付きログを対象とします。エラー検出パターンは以下のキーワードを含みます。
キーワード | 説明 |
---|---|
error | 一般的なエラー |
fail | 失敗メッセージ |
fatal | 致命的エラー |
warning / warn | 警告メッセージ |
killing | プロセス強制終了 |
除外するログパターンは設定ファイルで定義し、監視対象から除外することで誤検知を防ぎます。
実装・ロジック
本章では、監視スクリプトの実装構造と処理の流れを解説します。全体処理のフローやモードごとの動作仕様、主要関数の役割、状態遷移と例外処理の設計を明確にすることで、運用時の理解と改修の容易さを確保します。
全体処理フロー
監視スクリプトは、起動から終了まで以下の流れで処理を行います。
処理段階 | 概要 |
---|---|
前処理 | ログ出力の初期化、引数解析、ロックディレクトリとPIDファイルの設定 |
引数検証 | モードやユニット名、通知先、監視間隔などの妥当性を確認 |
メイン処理 | 指定されたモードに応じた関数を実行(常駐監視、単発監視、停止、一覧取得など) |
終了処理 | ロック解除、終了ログ出力、正常終了コードの返却 |
モード別動作仕様(start・run・once・stop・list・status)
スクリプトは実行時のモードに応じて動作が変わります。
モード | 動作概要 |
---|---|
start | ロック取得後、指定ユニットの存在確認を行い、-m runモードをバックグラウンド起動 |
run | 指定間隔で監視対象の稼働状態とjournaldログを確認し、障害検知時に通知します。 startモードから内部的に呼び出されるものであり、運用時に引数で直接指定して実行することはありません。 |
once | ロックを取らずに単発でログ監視を行い、検知結果を通知 |
stop | ロックディレクトリとPIDファイルを確認し、実行中の監視プロセスを終了 |
list | 現在起動中の監視ジョブ一覧を取得し、稼働状況を表示 |
status | 指定ユニットの監視プロセス稼働状況を表示 |
主要関数の役割と処理概要
checkJournald関数では、検出したエラーメッセージを一時ファイルに保存し、前回送信時の内容と比較します。
同一内容であれば送信をスキップし、時刻だけを更新することで、同じ障害メッセージが連続送信されることを防ぎます。これにより、障害継続時の通知スパムを抑止し、通知の実効性を高めます。
スクリプトは複数の主要関数で構成され、それぞれの役割は以下の通りです。
関数名 | 役割 |
---|---|
checkArgs | 引数の妥当性を検証し、不正時はusageを表示して終了 |
acquireLock / releaseLock | ロックディレクトリとPIDファイルの生成・削除で多重起動を防止 |
checkUnitExists | systemctlの一覧から監視対象ユニットの存在を確認 |
startMonitor | 監視プロセスのバックグラウンド起動とPID保存 |
runMonitor | 監視ループの実行、プロセス稼働確認、ログ監視関数の呼び出し |
onceMonitor | 単発でログ監視を行い、結果を通知 |
stopMonitor | 実行中の監視プロセスを終了し、ロックを解除 |
listMonitor | 稼働中の監視ジョブを一覧表示 |
checkJournald | journaldログとloggerタグ付きログからエラー・警告を抽出し、必要に応じて通知 |
sendToLine / sendToMail | 検知した障害内容をLINEまたはメールで通知 |
状態遷移と例外処理設計
監視スクリプトは、以下の状態遷移を基本とします。
現在状態 | イベント | 次状態 | 備考 |
---|---|---|---|
未起動 | startモード実行 | 監視中 | ロックディレクトリとPIDファイル生成 |
監視中 | 障害検知 | 監視中(通知後継続) | 通知送信後も監視を継続 |
監視中 | stopモード実行 | 未起動 | プロセス終了、ロック解除 |
監視中 | 異常終了 | 未起動(ロック残存) | 次回起動時に残存ロックを確認し処理 |
例外処理としては、引数不正時の即時終了、監視対象が存在しない場合のエラーログ出力、通知先設定ミス時のエラー出力などを行います。これにより、誤動作や意図しない監視開始を防止します。
Linuxサーバー障害検知・自動通知スクリプト
設計・仕様および処理ロジックに基づき、障害検知と通知を行う監視スクリプトの全文を掲載します。
このスクリプトはRHEL系Linux環境でsystemd管理下のサービスを対象に動作し、journaldログの監視とLINEまたはメールによる通知を行います。
コード内には主要処理ごとにコメントを付与しており、そのままコピーして環境に合わせた設定を行えば実運用が可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 |
#!/bin/sh #_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ # # スクリプト名: # send_alert.sh # # 使い方: # sh send_alert.sh -m {start|stop|status|list|run|once} -u <ユニット名> [-t mail|line] [-i 秒] # # 説明: # systemdユニット(例:postgresql-15)のjournaldログを監視し、 # しきい値やエラーパターン検出時に通知(メール/LINE)を行う。 # root権限での実行を前提。多重起動防止(ロック/PID管理)あり。 # # 主な引数: # -m 実行モード(start/stop/status/list/run/once) # -u 監視対象のsystemdユニット名(例:postgresql-15) # -t 通知手段(mail/line)省略可 # -i 監視間隔(秒)run/onceで使用 # # 実行例: # sh send_alert.sh -m start -u postgresql-15 -t mail -i 5 # sh send_alert.sh -m list # # 設計資料: # なし # #_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ # <変更履歴> # Ver. 変更管理No. 日付 更新者 変更内容 # 1.0 ---------- 2025/08/10 BePro 新規作成 #_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ # 共通関数・ログ読み込み . "$(dirname "$0")/../com/utils.shrc" . "$(dirname "$0")/../com/logger.shrc" setLANG utf-8 runAs root "$@" # ------------------------------------------------------------------ # 変数宣言 # ------------------------------------------------------------------ scope="var" host_id=$(hostname -s) LINE_CHANNEL_ACCESS_TOKEN=$(grep '^LINE_CHANNEL_ACCESS_TOKEN=' "${ETC_PATH}/${host_id}/.env" | cut -d '=' -f2-) LINE_CHANNEL_SECRET=$(grep '^LINE_CHANNEL_SECRET=' "${ETC_PATH}/${host_id}/.env" | cut -d '=' -f2-) MAIL_TO=$(grep '^MAIL_TO=' "${ETC_PATH}/${host_id}/.env" | cut -d '=' -f2-) mode="" unit_name="" target="line" # 通知先のデフォルトはline interval=60 # 監視間隔(秒)のデフォルト # ロック管理用ディレクトリ・PIDファイル lockD="" pidfile="" lock_owned=0 # ======================================== # 定数定義 # ======================================== readonly JOB_OK=0 readonly JOB_WR=1 readonly JOB_ER=2 # ------------------------------------------------------------------ # 関数定義 # ------------------------------------------------------------------ scope="func" # 終了処理:トラップからの呼び出し用 terminate() { # 自分がロックを取っている時だけ解放する releaseLock "${unit_name}" } # ------------------------------------------------------------------ # 関数名 :usage # 概要 :スクリプトの使用方法を表示して終了する # 説明 : # スクリプト実行時の引数指定方法を表示し、終了ステータス1で処理を終了する。 # 引数の誤りや不足がある場合に呼び出される想定。 # モード、監視対象ユニット名、通知先、監視間隔などの指定方法を提示する。 # # 引数 :なし(標準出力に使用方法を表示) # 戻り値 :1(異常終了) # 使用箇所 :引数チェック処理(checkArgs 関数など) # ------------------------------------------------------------------ usage() { cat <<EOF Usage: $0 -m {start|stop|status|once|list} -u <unit_name> [-t {line|mail}] [-i <interval>] -m : モード(start|stop|status|once|list) -u : systemdユニット名(必須) -t : 通知先(line または mail、デフォルト mail) -i : 監視間隔(秒、runモード時、デフォルト60) EOF exitLog ${JOB_WR} } # ------------------------------------------------------------------ # 関数名 :acquireLock # 概要 :ロックディレクトリとPIDファイルを用いた二重起動防止処理 # 説明 : # ・ロックディレクトリが存在しなければ作成する # ・PIDファイルが存在する場合はプロセス稼働状況を確認 # 稼働中なら1を返し終了(起動中) # 稼働していなければPIDファイルを削除して再取得 # ・新しいPIDファイルに自身のPIDを書き込み正常終了 # 引数 :なし(lockD, pidfile は事前定義されていること) # 戻り値 :0 正常取得 # 1 既に起動中またはディレクトリ作成失敗 # 使用箇所 :startMonitor など常駐監視の開始処理 # ------------------------------------------------------------------ acquireLock() { logOut "DEBUG" "$0:acquireLock() STARTED !" # ロックディレクトリがなければ作成 if [ ! -d "$lockD" ]; then logOut "DEBUGG" "ディレクトリを新規作成します。${lockD}" mkdir -p "$lockD" || return 1 fi # 既存PID確認 if [ -f "$pidfile" ]; then local pid pid=$(cat "$pidfile") if ps -p "$pid" > /dev/null 2>&1; then return 1 # 既に起動中 else # プロセス死んでたらクリア rm -f "$pidfile" fi fi echo $$ > "$pidfile" logOut "DEBUG" "$0:acquireLock() ENDED !" return 0 } # ------------------------------------------------------------------ # 関数名 :releaseLock # 概要 :ロックディレクトリと関連ファイルの削除処理 # 説明 : # ロックディレクトリ内のPIDファイルやnohupログを削除し、 # その後ディレクトリを安全に削除する。 # 削除対象はTMP_PATH配下の「*.lock」ディレクトリに限定し、 # 誤削除を防止するためにパスチェックを行う。 # # 引数 :なし(グローバル変数 lockD, pidfile を使用) # 戻り値 :なし # 使用箇所 :stopMonitor 関数など、監視停止処理や終了処理時 # ------------------------------------------------------------------ releaseLock() { # 既知のファイルを個別に削除 rm -f "${pidfile}" 2>/dev/null || true rm -f "${lockD}/nohup.log" 2>/dev/null || true rm -f "${lockD}/nohup.out" 2>/dev/null || true # 万一の取りこぼし(隠しファイル等)を掃除してからディレクトリ削除 if [ -d "${lockD}" ]; then # TMP_PATH 配下かつ 「<unit>.lock」形式だけを安全に削除 case "${lockD}" in "${TMP_PATH}/"*.lock) # 既存ファイルを全削除(意図せぬパス破壊を避けるため -rf は限定的に) rm -f "${lockD}/"* "${lockD}"/.[!.]* "${lockD}"/..?* 2>/dev/null || true rmdir "${lockD}" 2>/dev/null || true ;; esac fi } # ------------------------------------------------------------------ # 関数名 :checkArgs # 概要 :コマンドライン引数の検証処理 # 説明 : # モード(-m)やユニット名(-u)、監視間隔(-i)、通知先(-t)の # 必須性と値の妥当性をチェックする。 # 不正または不足があれば usage 関数を呼び出して終了する。 # # 引数 :なし(グローバル変数 mode, unit_name, interval, target を使用) # 戻り値 :なし(不正時は usage 関数で終了) # 使用箇所 :スクリプト実行開始直後の引数解析後 # ------------------------------------------------------------------ checkArgs() { if [ -z "${mode}" ]; then logOut "ERROR" "モード(-m)が指定されていません。" usage fi case "${mode}" in start|stop|status|once) if [ -z "${unit_name}" ]; then logOut "ERROR" "モード[${mode}]では -u ユニット名が必須です。" usage fi ;; run) if [ -z "${unit_name}" ]; then logOut "ERROR" "モード[run]では -u ユニット名が必須です。" usage fi if ! echo "${interval}" | grep -qE '^[0-9]+$'; then logOut "ERROR" "監視間隔(-i)は正の整数で指定してください。" usage fi if [ "${target}" != "line" ] && [ "${target}" != "mail" ]; then logOut "ERROR" "通知先(-t)は 'line' か 'mail' を指定してください。" usage fi ;; list) # listモードは特に必須引数なし ;; *) logOut "ERROR" "不正なモードが指定されました: ${mode}" usage ;; esac } # ------------------------------------------------------------------ # 関数名 :checkUnitExists # 概要 :指定された systemd ユニットの存在確認 # 説明 : # 引数やグローバル変数で指定されたユニット名が # systemctl list-unit-files の一覧に存在するかを確認する。 # 存在しない場合はエラーログを出力して終了する。 # # 引数 :なし(グローバル変数 unit_name を使用) # 戻り値 :なし(存在しない場合は exit 1 で終了) # 使用箇所 :startMonitor、runMonitor などユニット操作前の検証処理 # ------------------------------------------------------------------ checkUnitExists() { if ! systemctl list-unit-files --type=service --no-legend | awk '{print $1}' | grep -qw "${unit_name}"; then logOut "ERROR" "ユニット [${unit_name}] は存在しません。" exit 1 fi } # ------------------------------------------------------------------ # 関数名 :runMonitor # 概要 :監視プロセスの常駐監視処理 # 説明 : # 指定されたユニットのプロセス状態を定期的に確認し、 # 必要に応じてアラートを送信する。 # 引数 :なし # 戻り値 :なし(無限ループ) # 使用箇所 :main-process(-m run 時) # ------------------------------------------------------------------ runMonitor() { logOut "DEBUG" "$0:runMonitor() STARTED !" while true; do if ! pgrep -f "${unit_name}" > /dev/null 2>&1; then logOut "ERROR" "[${unit_name}] プロセスが停止しています。アラート送信します。" fi checkJournald logOut "DEBUG" "${interval}" sleep "${interval}" done logOut "DEBUG" "$0:runMonitor() ENDED !" } # ------------------------------------------------------------------ # 関数名 :startMonitor # 概要 :監視常駐プロセスの起動 # 説明 : # ・acquireLock が 1 を返した場合は「既に起動中」と判断して警告終了 # ・ロック取得後にユニット存在確認/作業ディレクトリ準備 # ・nohup で -m run をバックグラウンド起動し、実PIDを保存 # 引数 :なし(unit_name, target, interval, lockD, pidfile 等は事前定義) # 戻り値 :終了コードは exitLog に委譲(正常:JOB_OK/警告:JOB_WR/異常:JOB_ER) # 使用箇所 :-m start # ------------------------------------------------------------------ startMonitor() { logOut "DEBUG" "$0:startMonitor() STARTED !" # acquireLock 成功時のみ進む(1=既に起動中) if ! acquireLock "${unit_name}"; then logOut "WARN" "すでに監視が起動中です。" logOut "DEBUG" "$0:startMonitor() ENDED !" exitLog ${JOB_WR} fi checkUnitExists prepareDir "${lockD}" # 監視プロセス起動(バックグラウンド) logOut "DEBUG" "nohup ${BIN_PATH}/${SCRIPT_NAME} -m run -u ${unit_name} -t ${target} -i ${interval} > ${lockD}/nohup.log 2>&1 &" nohup "${BIN_PATH}/${SCRIPT_NAME}" -m run -u "${unit_name}" -t "${target}" -i "${interval}" > "${lockD}/nohup.log" 2>&1 & child_pid=$! # 生存確認(最大3回・約3秒) pid="" for i in 1 2 3; do if ps -p "${child_pid}" >/dev/null 2>&1; then pid="${child_pid}" break fi sleep 1 done if [ -z "${pid}" ]; then logOut "ERROR" "監視プロセスの起動に失敗しました。nohupログを確認してください。" logOut "DEBUG" "$0:startMonitor() ENDED !" exitLog ${JOB_ER} fi echo "${pid}" > "${pidfile}" logOut "INFO" "監視プロセスを起動しました。PID: ${pid}" logOut "DEBUG" "$0:startMonitor() ENDED !" } # ------------------------------------------------------------------ # 関数名 :stopMonitor # 概要 :監視スクリプトを終了させる # 説明 : # ・指定されたユニット名に対応するロックディレクトリとPIDファイルを確認 # ・PIDファイルのプロセスが稼働中なら終了させる # ・ロックを解除して監視状態を停止 # # 引数 :なし(事前に unit_name 変数が設定されていること) # 戻り値 :正常終了=0 / 異常終了=2 # 使用箇所 :send_alert.sh の main-process 内 # ------------------------------------------------------------------ stopMonitor() { logOut "DEBUG" "$0:stopMonitor() 開始" # ロックディレクトリの存在確認 if [ ! -d "${lockD}" ]; then logOut "WARN" "監視は実行されていません: ${unit_name}" exitLog ${JOB_WR} fi # PIDファイル存在確認 if [ ! -f "${pidfile}" ]; then logOut "ERROR" "PIDファイルが存在しません: ${pidfile}" exitLog ${JOB_ER} fi pid=$(cat "${pidfile}") if [ -z "${pid}" ]; then logOut "ERROR" "PIDが取得できません: ${pidfile}" exitLog ${JOB_ER} fi # プロセス稼働確認 if ps -p "${pid}" >/dev/null 2>&1; then logOut "DEBUG" "kill -9 ${pid}" kill -9 "${pid}" >/dev/null 2>&1 logOut "INFO" "監視プロセス(${pid})を終了しました。" else logOut "WARN" "監視プロセスはすでに存在しません: PID=${pid}" fi # ロック解除 releaseLock logOut "DEBUG" "$0:stopMonitor() 終了" } # ------------------------------------------------------------------ # 関数名 :statusMonitor # 概要 :監視プロセスの稼働状況確認 # 説明 : # PIDファイルと実プロセスの存在を確認して稼働状況を出力する。 # 引数 :なし # 戻り値 :0=起動中, 1=未起動 # 使用箇所 :main-process(-m status 時) # ------------------------------------------------------------------ statusMonitor() { logOut "DEBUG" "$0:statusMonitor() STARTED !" checkUnitExists if [ -f "${pidfile}" ]; then pid=$(cat "${pidfile}") if ps -p "${pid}" > /dev/null 2>&1; then logOut "INFO" "監視プロセスは起動中です。PID: ${pid}" logOut "DEBUG" "$0:statusMonitor() ENDED !" return 0 else logOut "INFO" "監視プロセスは停止しています。(PIDファイルのみ存在)" logOut "DEBUG" "$0:statusMonitor() ENDED !" return 1 fi else logOut "INFO" "監視プロセスは起動していません。" logOut "DEBUG" "$0:statusMonitor() ENDED !" return 1 fi } # ------------------------------------------------------------------ # 関数名 :onceMonitor # 概要 :単発実行による障害検知と通知処理 # 説明 : # バックグラウンドで常駐監視が動作している場合でも、 # ロックを取得せずに強制的にログ監視処理(checkJournald)を実行します。 # 実行後はロック解除処理を行い、単発監視の結果を通知します。 # 定期監視ではなく即時確認が必要な場合に利用します。 # # 引数 :なし # 戻り値 :なし # 使用箇所 :main-process(-m once 実行時) # ------------------------------------------------------------------ onceMonitor() { logOut "DEBUG" "$0:onceMonitor() STARTED" checkUnitExists # バックグラウンド監視が動いていても強制実行(ロックを取らない) checkJournald "once" # ロック解除 releaseLock logOut "DEBUG" "$0:onceMonitor() ENDED" } # ------------------------------------------------------------------ # 関数名 :checkJournald # 概要 :指定されたユニットの systemd ログおよび logger タグ付きログから # エラーや警告を抽出し、必要に応じて通知を送信する。 # 説明 : # - systemd の `-u`(ユニット名)と `-t`(logger タグ)の両方からログを取得し、 # エラー/警告パターンにマッチする行を抽出する。 # - 前回実行時刻からの差分のみを読み込むことで、過去ログの重複検出を防ぐ。 # - 同一内容のエラーは再送信せず、初回または変化があった場合のみ通知する。 # - 取得ログはマージし、重複行を削除してから通知する。 # - 通知方法は mail または line を選択可能。 # # 引数 :なし(グローバル変数 unit_name, TMP_PATH, target を使用) # 戻り値 :なし(処理結果はログ出力・通知) # 使用箇所 :send_alert.sh 内の監視ループや once 実行時 # ------------------------------------------------------------------ checkJournald() { logOut "DEBUG" "$0:checkJournald() STARTED" exclude_file="/home/bepro/projects/scripts/etc/exclude_patterns_send_alert.conf" local mode="${1:-run}" local pattern="error|fail|fatal|warning|warn|killing|【.*ERROR.*】| grep -viF ${IGNORE}" local last_msg_file="${TMP_PATH}/checkJournald_${unit_name}.last" local since_file="${TMP_PATH}/journal_since_${unit_name}.ts" # 直近だけ読む(初回は15分前)。以降は前回UNIX時刻から。 local since_opt if [ -s "${since_file}" ]; then since_opt="--since @$(cat "${since_file}")" else since_opt="--since now-15min" fi # -u (systemd) と -t (logger -p のタグ) を別々に取得 local systemd_logs logger_logs message systemd_logs=$(journalctl -u "${unit_name}" ${since_opt} --no-pager -o cat | grep -iE "${pattern}" | grep -v -f "$exclude_file" 2>/dev/null || true) logger_logs=$(journalctl -t "${unit_name}" ${since_opt} --no-pager -o cat | grep -iE "${pattern}" | grep -v -f "$exclude_file" 2>/dev/null || true) # マージ・重複排除・上限 message=$(printf "%s\n%s" "${systemd_logs}" "${logger_logs}" \ | sed '/^[[:space:]]*$/d' | sort -u | tail -n 200) logOut "DEBUG" "MESSAGE:${message}" # 検出なし → 復帰扱い(前回内容を消して時刻だけ前進) if [ -z "${message}" ]; then [ -f "${last_msg_file}" ] && rm -f "${last_msg_file}" date +%s > "${since_file}" logOut "DEBUG" "エラーは検出されませんでした。" logOut "DEBUG" "$0:checkJournald() ENDED" return fi # 同一内容は送らない(ただし時刻は前進) if [ -f "${last_msg_file}" ] && diff -q "${last_msg_file}" - <<< "${message}" >/dev/null 2>&1; then date +%s > "${since_file}" logOut "DEBUG" "同一エラーメッセージのため送信をスキップ" logOut "DEBUG" "$0:checkJournald() ENDED" return fi # 保存&通知 printf "%s\n" "${message}" > "${last_msg_file}" case "${target}" in line) sendToLine "${message}" ;; mail) sendToMail "${message}" ;; *) logOut "ERROR" "通知先が不明: ${target}" ;; esac # 次回用の since(境界落ち防止で -1 秒) ts_now="$(date +%s)"; printf "%s\n" "$((ts_now-1))" > "${since_file}" logOut "DEBUG" "$0:checkJournald() ENDED" } # ------------------------------------------------------------------ # 関数名 :listMonitor # 概要 :現在実行中の監視ジョブ一覧を表示する # 説明 : # TMP_PATH配下の *.lock ディレクトリをスキャンし、 # その中の PID ファイルを読み込んで稼働状況を出力する。 # 引数 :なし # 戻り値 :0=正常, 1=未起動 # 使用箇所 :main-process(-m list 時) # ------------------------------------------------------------------ listMonitor() { logOut "DEBUG" "$0:listMonitors() STARTED !" local found=0 for lock_dir in "${TMP_PATH}"/*.lock; do [ ! -d "$lock_dir" ] && continue local unit unit=$(basename "$lock_dir" .lock) local pidfile="$lock_dir/${unit}_pid" if [ -f "$pidfile" ]; then local pid pid=$(cat "$pidfile") if ps -p "$pid" > /dev/null 2>&1; then logOut "INFO" "起動中 (PID: ${pid}) [${unit}] " else logOut "WARN" "停止中 (PIDファイルあり) [${unit}] " fi found=1 else logOut "WARN" "PIDファイルなし [${unit}] " found=1 fi done [ $found -eq 0 ] && logOut "INFO" "現在起動中の監視ジョブはありません。" logOut "DEBUG" "$0:listMonitors() ENDED !" } # ------------------------------------------------------------------ # 関数名 :sendToLine # 概要 :LINE通知処理 # 説明 : # LINE Notify等のAPIを利用してメッセージを送信します。 # 引数 :$1 - 送信するメッセージ内容 # 戻り値 :なし # 使用箇所 :checkJournald # ------------------------------------------------------------------ sendToLine() { logOut "DEBUG" "$0:sendToLine() STARTED !" # 必要な環境変数の確認 if [ -z "${LINE_CHANNEL_ACCESS_TOKEN}" ]; then logOut "ERROR" "LINE_CHANNEL_ACCESS_TOKEN が未設定です。etc/${host_id}/.env を確認してください。" return 1 fi if [ -z "$1" ]; then logOut "ERROR" "送信メッセージが指定されていません。" return 1 fi # 引数のメッセージを行単位で送信(長文は分割) echo "$1" | while IFS= read -r line || [ -n "$line" ]; do # JSON用に最低限のエスケープ(\ と ") esc=$(printf '%s' "$line" | sed 's/\\/\\\\/g; s/"/\\"/g') payload=$(printf '{"messages":[{"type":"text","text":"%s"}]}' "$esc") if curl -sS -X POST "https://api.line.me/v2/bot/message/broadcast" \ -H "Authorization: Bearer ${LINE_CHANNEL_ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -d "${payload}" >/dev/null 2>&1; then logOut "INFO" "[LINE/MessagingAPI] broadcast OK: ${line}" else logOut "ERROR" "[LINE/MessagingAPI] broadcast NG: ${line}" fi # API連投対策 sleep 0.2 done logOut "DEBUG" "$0:sendToLine() ENDED !" } # ------------------------------------------------------------------ # 関数名 :sendToMail # 概要 :メール通知処理 # 説明 : # mailコマンド等を利用して通知メールを送信します。 # 引数 :$1 - 送信するメッセージ内容 # 戻り値 :なし # 使用箇所 :checkJournald # ------------------------------------------------------------------ sendToMail() { logOut "DEBUG" "$0:sendToMail() STARTED !" logOut "INFO" "(MAIL) $1" echo "$1" | mail -s "[ErrorLog] ${unit_name}" $MAIL_TO logOut "DEBUG" "MAIL_TO:${MAIL_TO}" logOut "DEBUG" "$0:sendToMail() ENDED !" } # ---------------------------------------------------------- # pre-process # ---------------------------------------------------------- scope="pre" startLog trap "terminate" 1 2 3 15 # 引数解析 while getopts ":m:u:t:i:" opt; do case "$opt" in m) mode="$OPTARG" ;; u) unit_name="$OPTARG" ;; t) target="$OPTARG" ;; i) interval="$OPTARG" ;; *) usage ;; esac done # lockD設定はunit_nameが決まってから lockD="${TMP_PATH}/${unit_name}.lock" pidfile="${lockD}/${unit_name}_pid" checkArgs # ---------------------------------------------------------- # main-routine # ---------------------------------------------------------- scope="main" case "${mode}" in start) startMonitor ;; stop) stopMonitor ;; status) statusMonitor ;; run) runMonitor ;; once) onceMonitor ;; list) listMonitor ;; *) usage ;; esac # ------------------------------------------------------------------ # post-process(終了処理) # ------------------------------------------------------------------ scope="post" exitLog ${JOB_OK} |
実行例・活用法
本章では、監視スクリプトを実際に運用するための手順と活用方法を解説します。初期設定から実行例、運用中のチューニング方法、そして障害発生時のトラブルシューティングまでをまとめています。
前提となる実行環境
Beエンジニアでシェルスクリプトを実行する環境は下記の通りとします。
実行環境
BASE_DIR(任意のディレクトリ)
- scripts
- bin(実行スクリプト格納領域)
- <<各種実行スクリプト>>.sh (実行ファイル)
- com(共通スクリプト格納領域)
- logger.shrc(共通ログ出力ファイル)
- utils.shrc(共通関数定義ファイル)
- etc(設定ファイル等の格納領域)
- infraMessage.conf(メッセージ定義ファイル)
- log(スクリプト実行ログの格納領域)
- スクリプト名.log
- tmp(テンポラリ領域)
- rep(レポート出力領域)
- bin(実行スクリプト格納領域)
初期設定と事前準備手順
スクリプトを運用する前に、環境構築と必要な設定を行います。ディレクトリ構成を作成します。
mkdir -p /home/user/projects/scripts/{bin,com,etc,log,tmp}
共通ライブラリを配置します。(logger.shrc、utils.shrc)監視スクリプト(send_alert.sh)をbinディレクトリに配置します。
ホスト名ごとの.envファイルをetc/<ホスト名>/配下に作成します。
LINE_CHANNEL_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx MAIL_TO=admin@example.com
実行権限を付与します。
chmod +x /home/user/projects/scripts/bin/send_alert.sh
基本的な実行例
監視スクリプトは複数のモードで利用できます。主な実行例を以下に示します。
目的 | コマンド例 |
---|---|
常駐監視を開始 | sh bin/send_alert.sh -m start -u postgresql-15 -t line -i 60 |
監視状態を確認 | sh bin/send_alert.sh -m status -u postgresql-15 |
常駐監視を停止 | sh bin/send_alert.sh -m stop -u postgresql-15 |
単発監視を実行 | sh bin/send_alert.sh -m once -u postgresql-15 |
監視ジョブ一覧を表示 | sh bin/send_alert.sh -m list |
運用時のチューニングと負荷対策
運用環境に合わせて以下の項目を調整することで、監視精度と負荷のバランスを取ることができます。
項目 | 推奨設定例 | 目的 |
---|---|---|
監視間隔(-i) | 60秒 | 負荷を抑えつつ障害検知までの遅延を最小化 |
ログ取得件数 | 直近15分 | 不要な過去ログの検出を防止 |
除外パターン設定 | etc/exclude_patterns_send_alert.conf | 誤検知や不要な通知の抑止 |
また、不要な監視対象や冗長な通知を削減することでシステム全体の負荷を軽減できます。
トラブルシューティングとFAQ
運用中によく発生する問題とその対処方法をまとめます。
症状 | 原因 | 対処方法 |
---|---|---|
監視が開始できない | ロックディレクトリが残存している | scripts/tmp配下の該当.lockディレクトリを削除 |
通知が届かない | LINEアクセストークンが誤っている | .envファイルの設定を確認し、再取得 |
メール通知が送信されない | メールサーバーが未設定または制限中 | postfixなどのMTA設定を確認、またはLINE通知に切り替え |
同じ障害が何度も通知される | 過去メッセージの比較ファイルが削除されている | checkJournaldのlastメッセージ保存機能を確認 |