ログファイルを監視して特定のキーワード(ERROR・CRITICALなど)が出力されたら自動でアラートを飛ばす、別の処理を起動する——これはシステム管理・バッチ自動化の定番パターンです。
この記事では、基本の findstr 監視から、既検知済みを再処理しない「差分検知」方式、複数ファイル横断監視、時間帯指定ループ、実践的な3つのシナリオまで完全解説します。
ログ監視の基本アーキテクチャ
バッチによるログ監視は次の3要素で構成されます。
| 要素 | 役割 | 主な実装手段 |
|---|---|---|
| 検索 | ログ内のキーワードを見つける | findstr |
| ループ制御 | 定期的に繰り返す・終了条件を管理する | goto + timeout |
| アクション | 検知後に実行する処理 | call・ログ記録・通知 |
findstr の詳細な使い方は「FINDSTRコマンドの使い方完全ガイド」を参照してください。
基本形:findstr でキーワードを検知して処理する
最もシンプルな監視ループです。指定した間隔でファイルをスキャンし、キーワードが見つかったらサブルーチンを呼びます。
@echo off
setlocal
set "LOGFILE=C:\logs\app.log"
set "KEYWORD=ERROR"
set "INTERVAL=10"
:LOOP
findstr /C:"%KEYWORD%" "%LOGFILE%" >nul 2>&1
if %errorlevel% equ 0 (
echo [%DATE% %TIME%] キーワード検知: %KEYWORD%
call :on_detect
)
timeout /t %INTERVAL% /nobreak >nul
goto :LOOP
:on_detect
echo [%DATE% %TIME%] 検知アクションを実行します
:: ここに検知後の処理を書く
goto :EOF
重要:「全体スキャン」の問題と差分検知
上記の基本形には重大な落とし穴があります。ファイル全体を毎回スキャンすると、過去に出力された ERROR も毎回検知してしまい、アクションが無限に実行されます。
:: NG: ファイル全体をスキャンすると過去のERRORも毎回検知してしまう
:LOOP
findstr /C:"ERROR" app.log >nul 2>&1
if %errorlevel% equ 0 (
:: ← 既存のERRORも検知してしまい、アクションが何度も実行される
call alert.bat
)
timeout /t 10 /nobreak >nul
goto :LOOP
解決策1:ファイルサイズ差分方式(シンプル)
前回のファイルサイズを記録しておき、サイズが増えたときだけスキャンします。新規に追記された部分だけを対象にできます。
@echo off
setlocal enabledelayedexpansion
set "LOGFILE=C:\logs\app.log"
set "KEYWORD=ERROR"
set "INTERVAL=10"
set "PREV_SIZE=0"
:LOOP
:: 現在のファイルサイズを取得
for %%F in ("%LOGFILE%") do set "CURR_SIZE=%%~zF"
:: サイズが増えていれば差分を処理
if !CURR_SIZE! GTR !PREV_SIZE! (
echo [%TIME%] ファイル更新を検知 (前回: !PREV_SIZE! → 今回: !CURR_SIZE!)
findstr /C:"%KEYWORD%" "%LOGFILE%" >nul 2>&1
if !errorlevel! equ 0 (
echo [%TIME%] %KEYWORD% を検知しました
call :on_detect
)
set "PREV_SIZE=!CURR_SIZE!"
)
timeout /t %INTERVAL% /nobreak >nul
goto :LOOP
:on_detect
echo [%DATE% %TIME%] アクション実行
goto :EOF
解決策2:行数追跡方式(より精密)
PowerShell で行数を取得し、前回から増加した行だけを for /f skip= で読み飛ばして処理します。追加された行のみを正確に対象にできます。
@echo off
setlocal enabledelayedexpansion
set "LOGFILE=C:\logs\app.log"
set "KEYWORD=ERROR"
set "INTERVAL=10"
set "PREV_LINES=0"
:LOOP
:: 現在の行数を取得(PowerShell使用)
for /f %%N in ('powershell -NoProfile -Command "(Get-Content -Path ''%LOGFILE%'').Count"') do set "CURR_LINES=%%N"
if !CURR_LINES! GTR !PREV_LINES! (
:: PowerShellで新規追加行だけを取得してキーワード検索
powershell -NoProfile -Command ^
"$f=Get-Content '%LOGFILE%';"
"$new=$f[%PREV_LINES%..($f.Count-1)];"
"$new | Select-String '%KEYWORD%'"
if !errorlevel! equ 0 (
echo [%DATE% %TIME%] 新規 %KEYWORD% を検知しました
call :on_detect
)
set "PREV_LINES=!CURR_LINES!"
)
timeout /t %INTERVAL% /nobreak >nul
goto :LOOP
:on_detect
echo [%DATE% %TIME%] アクション実行
goto :EOF
監視ループの終了条件を設定する
時間帯を指定して監視する(業務時間内のみ)
現在の時(HH)を取得して開始・終了時刻と比較します。%TIME: =0% で1桁時刻の先頭スペースを0に変換するのがポイントです。
@echo off
setlocal enabledelayedexpansion
set "LOGFILE=C:\logs\app.log"
set "KEYWORD=ERROR"
set "START_HH=09"
set "END_HH=18"
set "INTERVAL=30"
:LOOP
:: 現在の時(HH)を取得
for /f "tokens=1 delims=:" %%H in ("%TIME: =0%") do set "NOW_HH=%%H"
:: 監視時間帯チェック(START_HH <= NOW_HH < END_HH)
if !NOW_HH! LSS %START_HH% (
timeout /t 60 /nobreak >nul
goto :LOOP
)
if !NOW_HH! GEQ %END_HH% (
echo [%TIME%] 監視時間終了 (%END_HH%時以降)
exit /b 0
)
:: 監視処理
findstr /C:"%KEYWORD%" "%LOGFILE%" >nul 2>&1
if !errorlevel! equ 0 (
echo [%TIME%] %KEYWORD% 検知
call :on_detect
)
timeout /t %INTERVAL% /nobreak >nul
goto :LOOP
:on_detect
goto :EOF
ループ回数の上限を設定する
タスクスケジューラと組み合わせる場合は、回数制限を付けておくと安全です(10秒間隔×360回=1時間動作)。
@echo off
setlocal enabledelayedexpansion
set "LOGFILE=C:\logs\app.log"
set "KEYWORD=ERROR"
set "INTERVAL=10"
set "MAX_LOOPS=360"
set /a "COUNT=0"
:LOOP
set /a "COUNT+=1"
if !COUNT! GTR %MAX_LOOPS% (
echo [%TIME%] 上限回数(%MAX_LOOPS%回)に達したため終了します
exit /b 0
)
findstr /C:"%KEYWORD%" "%LOGFILE%" >nul 2>&1
if !errorlevel! equ 0 (
echo [%TIME%] %KEYWORD% 検知(%COUNT%回目のループ)
call :on_detect
)
timeout /t %INTERVAL% /nobreak >nul
goto :LOOP
:on_detect
goto :EOF
複数キーワードを同時に監視する
重大度に応じてキーワードを分けて処理できます。findstr に複数の /C: を指定するとOR検索になります。
@echo off
setlocal enabledelayedexpansion
set "LOGFILE=C:\logs\app.log"
set "INTERVAL=10"
:LOOP
:: 複数キーワードをORで検索(findstr に複数指定)
findstr /C:"ERROR" /C:"CRITICAL" /C:"FATAL" "%LOGFILE%" >nul 2>&1
if !errorlevel! equ 0 (
echo [%TIME%] 重大キーワードを検知
call :on_critical
)
:: WARNINGは別処理
findstr /C:"WARNING" "%LOGFILE%" >nul 2>&1
if !errorlevel! equ 0 (
echo [%TIME%] 警告キーワードを検知
call :on_warning
)
timeout /t %INTERVAL% /nobreak >nul
goto :LOOP
:on_critical
echo [%DATE% %TIME%] CRITICAL: アラートを発報します
goto :EOF
:on_warning
echo [%DATE% %TIME%] WARNING: ログに記録します
goto :EOF
複数ログファイルを横断して監視する
findstr /s でサブフォルダを含む複数ファイルを一度に検索できます。
複数ログの一括検索の詳細は「複数のログファイルを一括で検索する方法(findstr /s)」も参照してください。
@echo off
setlocal
:: /s: サブフォルダを含む全 .log ファイルを検索
set "LOG_DIR=C:\logs"
set "KEYWORD=ERROR"
findstr /s /c:"%KEYWORD%" "%LOG_DIR%\*.log" >nul 2>&1
if %errorlevel% equ 0 (
echo [%TIME%] %LOG_DIR% 配下でキーワードを検知しました
:: 検知したファイルと行を表示
findstr /s /n /c:"%KEYWORD%" "%LOG_DIR%\*.log"
)
endlocal
検知後のアクション:ログ記録
検知内容をタイムスタンプ付きで記録します。リダイレクト >> で追記します。
ログ出力の詳細は「バッチファイルで実行ログを出力する方法」、リダイレクトの詳細は「リダイレクト(> >> 2>&1)の使い方完全ガイド」を参照してください。
:on_detect :: 検知内容をタイムスタンプ付きで記録 set "ALERTLOG=C:\logs\alert_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log" echo [%DATE% %TIME%] ERROR検知: %~1 >> "%ALERTLOG%" goto :EOF
検知後のアクション:ポップアップ通知
対話環境で使う場合は、msg コマンドや PowerShell のバルーン通知を使えます。
:on_detect :: ポップアップダイアログで通知 msg * /time:30 "エラーを検知しました: %~1" :: PowerShell でバルーン通知(より目立たない形式) powershell -NoProfile -Command ^ "Add-Type -AssemblyName System.Windows.Forms;" "$n=New-Object System.Windows.Forms.NotifyIcon;" "$n.Icon=[System.Drawing.SystemIcons]::Warning;" "$n.Visible=$true;" "$n.ShowBalloonTip(5000,'ログ監視アラート','ERRORを検知しました','Warning');" "Start-Sleep -Seconds 6; $n.Dispose()" goto :EOF
実践例A:WebサーバーログのHTTP500を監視してサービスを自動再起動する
IIS/Apache のアクセスログに HTTP 500 エラーが一定回数以上出たら自動でサービスを再起動する例です。
@echo off
setlocal enabledelayedexpansion
:: =====================================================
:: IIS/Apache アクセスログの ERROR 監視バッチ
:: 検知時: アラートログ記録 + サービス再起動
:: =====================================================
set "LOGFILE=C:\inetpub\logs\LogFiles\W3SVC1\u_ex%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log"
set "ALERTLOG=C:\logs\alert\web_alert_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log"
set "KEYWORD=500"
set "INTERVAL=30"
set "PREV_SIZE=0"
set "ALERT_COUNT=0"
set "MAX_ALERTS=5"
:: アラートログ格納先を確認
if not exist "C:\logs\alert\" mkdir "C:\logs\alert"
echo [%DATE% %TIME%] 監視開始: %LOGFILE% >> "%ALERTLOG%"
:LOOP
if not exist "%LOGFILE%" (
timeout /t %INTERVAL% /nobreak >nul
goto :LOOP
)
for %%F in ("%LOGFILE%") do set "CURR_SIZE=%%~zF"
if !CURR_SIZE! GTR !PREV_SIZE! (
findstr /C:"%KEYWORD%" "%LOGFILE%" >nul 2>&1
if !errorlevel! equ 0 (
set /a "ALERT_COUNT+=1"
echo [%DATE% %TIME%] HTTP 500 検知 (通算: !ALERT_COUNT%回) >> "%ALERTLOG%"
:: 5回以上検知でサービス再起動
if !ALERT_COUNT! GEQ %MAX_ALERTS% (
echo [%DATE% %TIME%] サービス再起動を実行します >> "%ALERTLOG%"
net stop W3SVC
net start W3SVC
set /a "ALERT_COUNT=0"
)
)
set "PREV_SIZE=!CURR_SIZE!"
)
timeout /t %INTERVAL% /nobreak >nul
goto :LOOP
実践例B:完了フラグファイルを監視して後続処理を起動する
バッチ間の連携によく使われるパターンです。前処理が完了したことを示すフラグファイルを監視し、検知したら後続バッチを呼び出します。タイムアウト付きで無限待機を防ぎます。
@echo off
setlocal enabledelayedexpansion
:: =====================================================
:: 完了フラグファイルを監視して次の処理を起動する
:: 用途: バッチ間の連携・依存処理の制御
:: =====================================================
set "FLAG_FILE=C:\work\step1_done.flag"
set "NEXT_BAT=C:\work\step2_process.bat"
set "TIMEOUT_SEC=3600"
set "INTERVAL=10"
set /a "ELAPSED=0"
echo [%TIME%] step1 完了を待機中...
:WAIT
if exist "%FLAG_FILE%" (
echo [%TIME%] フラグを検知しました: %FLAG_FILE%
del "%FLAG_FILE%"
echo [%TIME%] step2 を起動します
call "%NEXT_BAT%"
goto :DONE
)
:: タイムアウトチェック
set /a "ELAPSED+=%INTERVAL%"
if !ELAPSED! GEQ %TIMEOUT_SEC% (
echo [%TIME%] [TIMEOUT] %TIMEOUT_SEC%秒待機しましたが完了しませんでした
exit /b 1
)
timeout /t %INTERVAL% /nobreak >nul
goto :WAIT
:DONE
echo [%TIME%] 処理完了
endlocal
実践例C:複数ログを横断してERROR件数の日次レポートを生成する
フォルダ内の全ログファイルを横断してERROR件数を集計し、日次レポートファイルに出力します。タスクスケジューラで毎日深夜に実行するのに適したパターンです。
ログローテーションの実装については「バッチファイルでログローテーションを実装する方法」も合わせてご覧ください。
@echo off
setlocal
:: =====================================================
:: 複数ログファイルを横断してERROR件数を集計
:: 日次レポートファイルに出力する
:: =====================================================
set "LOG_DIR=C:\logs"
set "REPORT=C:\reports\daily_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.txt"
if not exist "C:\reports\" mkdir "C:\reports"
echo ========================================= > "%REPORT%"
echo 日次ログ集計レポート: %DATE% >> "%REPORT%"
echo ========================================= >> "%REPORT%"
echo. >> "%REPORT%"
:: 各ログファイルのERROR件数をカウント
for %%F in ("%LOG_DIR%\*.log") do (
set "CNT=0"
for /f %%N in ('findstr /C:"ERROR" "%%F" ^| find /c /v ""') do set "CNT=%%N"
echo [%%~nxF] ERROR件数: !CNT! >> "%REPORT%"
)
echo. >> "%REPORT%"
echo --- 全ファイルを横断したERROR一覧 --- >> "%REPORT%"
findstr /s /n /c:"ERROR" "%LOG_DIR%\*.log" >> "%REPORT%"
echo レポート生成完了: %REPORT%
endlocal
タスクスケジューラと組み合わせる
ログ監視バッチをタスクスケジューラで定期起動する場合のポイントです。
- 「プログラムの開始」に
cmd.exe、引数に/c "C:\scripts\monitor.bat"を設定する - 「開始場所」にバッチファイルのあるフォルダを設定する(相対パス解決のため)
- 「最長実行時間」を設定して暴走を防ぐ(例: 1時間)
- ループ回数制限を組み込んでおくと、タスクスケジューラが1時間おきに再起動するパターンが安全
タスクスケジューラ連携の詳細は「Windowsタスクスケジューラと連携して定期処理を自動化する方法」を参照してください。
よくある落とし穴5選
落とし穴1:毎回全体スキャンで同じERRORを何度も処理する
最も頻発する問題です。ファイルサイズ差分方式か行数追跡方式で必ず対処してください。
:: NG: 毎回ファイル全体をfindstrすると過去のマッチも再検知する
:LOOP
findstr /C:"ERROR" app.log >nul 2>&1
if %errorlevel% equ 0 call alert.bat <- 何度も呼ばれる
timeout /t 10 /nobreak >nul
goto :LOOP
:: OK: ファイルサイズまたは行数を記録して差分だけ処理する
for %%F in ("app.log") do set "CURR_SIZE=%%~zF"
if !CURR_SIZE! GTR !PREV_SIZE! (
:: 新規部分だけを検索
findstr /C:"ERROR" app.log >nul 2>&1
if !errorlevel! equ 0 call alert.bat
set "PREV_SIZE=!CURR_SIZE!"
)
落とし穴2:ログファイルが存在しないときにfindstrがエラーを返す
ログファイルが作成される前から監視を開始すると、findstr のエラーが ERRORLEVEL に影響します。
:: NG: ログファイルがまだ存在しない状態でfindstrを実行
findstr /C:"ERROR" "C:\logs\app.log"
:: → ファイルが存在しないとfindstrがエラーを返し、
:: 後続の if errorlevel が誤動作する
:: OK: 事前にIF EXISTでファイルの存在を確認する
if exist "C:\logs\app.log" (
findstr /C:"ERROR" "C:\logs\app.log" >nul 2>&1
if %errorlevel% equ 0 echo 検知
) else (
echo ログファイルがまだ存在しません
)
落とし穴3:findstr /C: なしでスペース区切りのフレーズを検索する
findstr "DISK ERROR" はOR検索になります。フレーズ検索には /C: が必須です。
:: NG: /C: を使わないとスペース区切りで複数キーワードとして解釈される findstr "DISK ERROR" app.log :: → "DISK" または "ERROR" を含む行にマッチ(意図と異なる) :: OK: フレーズ検索は /C: を使う findstr /C:"DISK ERROR" app.log :: → "DISK ERROR" を含む行にのみマッチ
落とし穴4:ループ内で変更した変数を %変数% で参照する
遅延展開を有効にしてもループ内で変更した変数は %変数% では取れません。!変数! を使います。
:: NG: ループ内でsetした変数を同じ行で参照しようとする setlocal enabledelayedexpansion set /a COUNT=0 :LOOP set /a COUNT+=1 if %COUNT% GEQ 10 goto :END <- %COUNT% は展開前の値が使われる goto :LOOP :: OK: ループ内で変更した変数は !変数名! で参照する :LOOP set /a COUNT+=1 if !COUNT! GEQ 10 goto :END goto :LOOP
落とし穴5:タスクスケジューラから実行したときにパスが解決されない
相対パスはタスクスケジューラ環境で機能しません。%~dp0 ベースの絶対パスを使います。
:: NG: 相対パスでログファイルを指定するとタスクスケジューラから動かない set "LOGFILE=logs\app.log" findstr /C:"ERROR" "%LOGFILE%" :: → タスクスケジューラのカレントはSystem32になることが多く、ファイルが見つからない :: OK: バッチファイルの場所を基準にした絶対パスを使う set "LOGFILE=%~dp0logs\app.log" findstr /C:"ERROR" "%LOGFILE%" :: またはタスクスケジューラの「開始場所」にバッチのフォルダを設定する
よくある質問(FAQ)
>nul 2>&1 でリダイレクトしてもERRORLEVELは変わりません。ファイルが存在しない場合もERRORLEVEL=1になるため、IF EXISTで事前チェックしてください。Get-Content -ReadCount 0 を試してください。timeout /t 1 /nobreak >nul で1秒間隔にすることは可能です。ただし、ファイルサイズの確認とfindstrの実行がCPU負荷になるため、業務用途では5〜30秒が現実的です。真のリアルタイム監視には PowerShell の FileSystemWatcher を使う方が適しています。WScript.Shell.Run を使う方法もあります。chcp 65001 でUTF-8に切り替えてから findstr を実行するか、PowerShell の Get-Content -Encoding UTF8 | Select-String を使ってください。Send-MailMessage や、社内で利用可能な blat(フリーの SMTP クライアント)を使えばバッチから通知メールを送れます。まとめ
ログ監視バッチの構築パターンをまとめます。
| 目的 | 実装方法 |
|---|---|
| 基本のキーワード検知 | findstr /C:"キーワード" ファイル >nul 2>&1 |
| 差分のみ検知(再処理防止) | ファイルサイズ差分方式または行数追跡方式 |
| 複数キーワードOR検索 | findstr /C:"A" /C:"B" /C:"C" |
| 複数ファイル横断 | findstr /s /n /c:"キーワード" *.log |
| 時間帯制限 | 現在時(HH)を取得して開始・終了時刻と比較 |
| 回数制限 | set /a COUNT+=1 + 上限チェック |
| 定期自動実行 | タスクスケジューラ + ループ回数制限の組み合わせ |
差分検知を実装するかどうかが、実用に耐えるログ監視バッチと「なんとなく動く」バッチの分岐点です。本記事のファイルサイズ差分方式か行数追跡方式を採用してください。
関連記事: 「ERRORLEVEL を使ってエラーハンドリングを行う方法」 / 「バッチファイルでログローテーションを実装する方法」