Shellの基礎知識(実践編)

【RHEL系Linux】サーバーの障害検知と自動通知|systemdログ監視の実装例

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ログを自動で監視し、即時通知することで、復旧時間を短縮することを目的とします。

想定環境と前提条件

本記事で扱う構成は、以下の環境を前提としています。

項目内容
OSRHEL系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/tmpPIDファイルやロックディレクトリなど一時ファイルを保存

引数とオプション仕様

監視スクリプトは複数のモードとオプションを持ち、用途に応じた実行が可能です。

オプション説明必須
-m実行モード(start、stop、status、once、list)必須
-u監視対象のsystemdユニット名(例:postgresql-15)必須(list以外)
-t通知先(lineまたはmail、デフォルトはline)任意
-i監視間隔(秒、デフォルト60秒)任意(runモードで有効)

環境変数と設定ファイル構造

環境変数はホストごとに定義された.envファイルで管理します。このファイルにはLINEのAPIキーやメール送信先などの機密情報を格納します。

変数名用途
LINE_CHANNEL_ACCESS_TOKENLINE 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ファイルの生成・削除で多重起動を防止
checkUnitExistssystemctlの一覧から監視対象ユニットの存在を確認
startMonitor監視プロセスのバックグラウンド起動とPID保存
runMonitor監視ループの実行、プロセス稼働確認、ログ監視関数の呼び出し
onceMonitor単発でログ監視を行い、結果を通知
stopMonitor実行中の監視プロセスを終了し、ロックを解除
listMonitor稼働中の監視ジョブを一覧表示
checkJournaldjournaldログとloggerタグ付きログからエラー・警告を抽出し、必要に応じて通知
sendToLine / sendToMail検知した障害内容をLINEまたはメールで通知

状態遷移と例外処理設計

監視スクリプトは、以下の状態遷移を基本とします。

現在状態イベント次状態備考
未起動startモード実行監視中ロックディレクトリとPIDファイル生成
監視中障害検知監視中(通知後継続)通知送信後も監視を継続
監視中stopモード実行未起動プロセス終了、ロック解除
監視中異常終了未起動(ロック残存)次回起動時に残存ロックを確認し処理

例外処理としては、引数不正時の即時終了、監視対象が存在しない場合のエラーログ出力、通知先設定ミス時のエラー出力などを行います。これにより、誤動作や意図しない監視開始を防止します。

Linuxサーバー障害検知・自動通知スクリプト

設計・仕様および処理ロジックに基づき、障害検知と通知を行う監視スクリプトの全文を掲載します。

このスクリプトはRHEL系Linux環境でsystemd管理下のサービスを対象に動作し、journaldログの監視とLINEまたはメールによる通知を行います。

コード内には主要処理ごとにコメントを付与しており、そのままコピーして環境に合わせた設定を行えば実運用が可能です。

#!/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(レポート出力領域)

初期設定と事前準備手順

スクリプトを運用する前に、環境構築と必要な設定を行います。ディレクトリ構成を作成します。

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メッセージ保存機能を確認

よく読まれている記事

1

「私たちが日々利用しているスマートフォンやインターネット、そしてスーパーコンピュータやクラウドサービス――これらの多くがLinuxの力で動いていることをご存じですか? 無料で使えるだけでなく、高い柔軟 ...

2

Linux環境でよく目にする「Vim」という名前。サーバーにログインしたら突然Vimが開いてしまい、「どうやって入力するの?」「保存や終了ができない!」と困った経験をした人も多いのではないでしょうか。 ...

3

ネットワーク技術は現代のITインフラにおいて不可欠な要素となっています。しかし、ネットワークを深く理解するためには、その基本となる「プロトコル」と「レイヤ」の概念をしっかり把握することが重要です。 こ ...

4

この記事は、Linuxについて勉強している初心者の方向けに「Shellスクリプト」について解説します。最後まで読んで頂けましたら、Shellスクリプトはどのような役割を担っているのか?を理解出来るよう ...

5

Javaは世界中で広く使われているプログラミング言語であり、特に業務システムやWebアプリケーションの開発において欠かせない存在です。本記事では、初心者向けにJavaの基礎知識を網羅し、環境構築から基 ...

-Shellの基礎知識(実践編)