ログ解析スクリプトを「ただの使い捨てツール」で終わらせるな:FSOを用いた堅牢な異常検知設計論
「ログファイルを読み込んで、エラーの回数を数える。そんなの、数行のコードで書けるじゃないか」
もし君がそう考えているなら、君はまだ「自動化エンジニア」ではなく、単なる「スクリプトの書き手」に過ぎない。
実務、それもミッションクリティカルな運用環境において、スクリプトは「動けばいい」のではない。「予測不能な事態に直面しても、システムを壊さず、正しく失敗し、確実に結果を報告すること」が求められる。
今回は、FileSystemObject (FSO) を用いたログ解析において、中級者が陥りがちな罠を回避し、プロフェッショナルが設計すべき「堅牢な集計・通知システム」の極意を伝授する。
1. 素人が書くコード、プロが設計するアーキテクチャ
多くの開発者が書く「典型的な失敗例」は、以下のような構造だ。
- ファイルを開く $\rightarrow$ 全行ループ $\rightarrow$ 文字列比較 $\rightarrow$ カウント $\rightarrow$ 終了。
これでは、以下のリスクを制御できない。
1. ファイルロック問題: ログ出力プロセスがファイルを開いている最中に、読み取り権限エラーでスクリプトが即死する。
2. メモリ枯渇: 数GBある巨大なログファイルを `ReadAll` で一気にメモリに展開し、サーバーをフリーズさせる。
3. エラーの隠蔽: `On Error Resume Next` を乱用し、なぜ解析に失敗したのか分からぬまま「異常なし」として処理を終えてしまう。
プロフェッショナルは、「リソースの局所化」「ストリーム処理による低メモリ消費」「厳格なエラーハンドリング」を設計の根幹に据える。
2. 堅牢な実装のための3つの鉄則
① Dictionaryオブジェクトによる集計の高速化
`If`文を重ねて変数を増やすような泥臭い実装はやめろ。`Scripting.Dictionary` を使い、キーワードをキー、出現回数を値として管理せよ。これにより、監視対象のログレベル(FATAL, WARN, ERROR等)が増えたとしても、コードのロジックを変更せずに拡張できる。
② Line-by-Line 処理の徹底
巨大なログファイルは、メモリの敵だ。`ReadLine` メソッドを用い、一行ずつ読み込みながら判定を行う。これにより、メモリ使用量を一定(定数レベル)に保ち、ファイルサイズに依存しないパフォーマンスを実現できる。
③ 「失敗」を設計に組み込む
ファイルが存在しない、読み取り権限がない、ディスクフルで書き込めない……。これらの例外を「想定外」として扱うな。すべての例外に対して、適切なログ出力または通知を行う構造を構築せよ。
3. プロダクション・クオリティのコード例
以下に、実務でそのまま利用可能な、堅牢性を担保したVBScriptのテンプレートを示す。これは単なるサンプルではない。保守性と信頼性を考慮した設計図だ。
' ==============================================================================
' Script Name: LogAnalyzer_Pro.vbs
' Description: FSOを用いたログ解析および閾値超過検知システム
' Author: Chief Architect
' ==============================================================================
Option Explicit
' --- 設定領域 (Configuration) ---
' 本来は外部のINIファイルや環境変数から読み込むべきだが、ここでは定数として定義
Const TARGET_LOG_PATH = "C:\Logs\app_server.log"
Const SUMMARY_OUT_PATH = "C:\Logs\alert_summary.txt"
Const THRESHOLD_FATAL = 1 ' FATALは1回でも即通知
Const THRESHOLD_WARN = 5 ' WARNは5回以上で通知
Const LOG_LEVEL_FATAL = "FATAL"
Const LOG_LEVEL_WARN = "WARN"
' --- メインルーチン ---
Main
Sub Main()
Dim fso, dictCounts, strSummary
Dim errCode, errDesc
Set fso = CreateObject("Scripting.FileSystemObject")
Set dictCounts = CreateObject("Scripting.Dictionary")
' 1. 初期化: カウント用の辞書をセットアップ
dictCounts.Add LOG_LEVEL_FATAL, 0
dictCounts.Add LOG_LEVEL_WARN, 0
' 2. 解析処理の実行
On Error Resume Next
Call AnalyzeLogFile(fso, dictCounts)
If Err.Number <> 0 Then
errCode = Err.Number
errDesc = Err.Description
On Error GoTo 0
Call HandleFatalError("解析中に致命的なエラーが発生しました: " & errDesc & " (Code: " & errCode & ")")
Exit Sub
End If
On Error GoTo 0
' 3. 閾値判定とサマリー生成
strSummary = GenerateSummary(dictCounts)
' 4. 結果の出力(閾値を超えた場合のみ書き出し)
If strSummary <> "" Then
If Not WriteSummaryFile(fso, strSummary) Then
' 書き出し失敗は運用上の重大な問題
Call HandleFatalError("サマリーファイルの書き出しに失敗しました。")
End If
End If
' 5. 後処理 (Cleanup)
Set dictCounts = Nothing
Set fso = Nothing
' WScript.Echo "Analysis completed successfully." ' デバッグ用
End Sub
' --- 解析エンジン ---
Sub AnalyzeLogFile(objFSO, objDict)
Dim objFile, strLine
' ファイル存在チェック
If Not objFSO.FileExists(TARGET_LOG_PATH) Then
Err.Raise 1, "AnalyzeLogFile", "Target log file not found: " & TARGET_LOG_PATH
End If
' ファイルを読み取り専用(1)でオープン
' ※ 他のプロセスが書き込み中でも、読み取りモードならアクセス可能なケースが多い
Set objFile = objFSO.OpenTextFile(TARGET_LOG_PATH, 1)
Do Until objFile.AtEndOfStream
strLine = objFile.ReadLine
' キーワード判定(大文字小文字を区別しない場合は UCase を使用)
If InStr(strLine, LOG_LEVEL_FATAL) > 0 Then
objDict(LOG_LEVEL_FATAL) = objDict(LOG_LEVEL_FATAL) + 1
ElseIf InStr(strLine, LOG_LEVEL_WARN) > 0 Then
objDict(LOG_LEVEL_WARN) = objDict(LOG_LEVEL_WARN) + 1
End If
Loop
objFile.Close
Set objFile = Nothing
End Sub
' --- 判定ロジック ---
Function GenerateSummary(objDict)
Dim strResult, fatalCount, warnCount
fatalCount = objDict(LOG_LEVEL_FATAL)
warnCount = objDict(LOG_LEVEL_WARN)
strResult = ""
' 閾値判定
If fatalCount >= THRESHOLD_FATAL Or warnCount >= THRESHOLD_WARN Then
strResult = "[ALERT] Log Threshold Exceeded" & vbCrLf & _
"Timestamp: " & Now & vbCrLf & _
"--------------------------------" & vbCrLf & _
"FATAL count: " & fatalCount & " (Threshold: " & THRESHOLD_FATAL & ")" & vbCrLf & _
"WARN count: " & warnCount & " (Threshold: " & THRESHOLD_WARN & ")" & vbCrLf & _
"--------------------------------" & vbCrLf
End If
GenerateSummary = strResult
End Function
' --- 出力エンジン ---
Function WriteSummaryFile(objFSO, strContent)
On Error Resume Next
Dim objFile
' ForWriting (2) を使用。既存ファイルがある場合は上書き。
' 運用要件に応じて ForAppending (8) への変更を検討せよ。
Set objFile = objFSO.CreateTextFile(SUMMARY_OUT_PATH, True)
If Err.Number <> 0 Then
WriteSummaryFile = False
Else
objFile.WriteLine strContent
objFile.Close
WriteSummaryFile = True
End If
On Error GoTo 0
End Function
' --- エラーハンドリング ---
Sub HandleFatalError(strMsg)
' 本来はここでイベントログへの書き込みや、管理者へのメール送信を行う
' ここでは標準エラー出力へ
WScript.StdErr.WriteLine "[CRITICAL ERROR] " & strMsg
' 運用監視ツール(Zabbix/Nagios等)が戻り値を監視できるよう、終了コードを返す
WScript.Quit 1
End Sub
4. チーフアーキテクトの視点:さらに高みを目指すために
このスクリプトを実戦投入するにあたり、以下の「運用上の勘所」を忘れないでほしい。
文字コードの罠
FSOの `OpenTextFile` は、基本的に ANSI (Shift-JIS) を前提としている。もし解析対象のログが UTF-8 で出力されている場合、このスクリプトは文字化けを起こし、キーワード検知に失敗する。
現代的なシステムではUTF-8が標準だ。その場合は、`ADODB.Stream` オブジェクトを使用し、`Charset = “UTF-8″` を明示的に指定する設計に切り替えろ。
冪等性(Idempotency)の確保
このスクリプトをタスクスケジューラで5分おきに実行する場合、同じエラーを何度もカウントし続けていないか?
「前回どこまで読み込んだか」を管理する チェックポイント・ファイル(ポインタ管理) を導入する設計にステップアップせよ。前回の最終行番号を記録しておけば、差分のみを高速かつ正確にスキャンできる。
終了コードの重要性
`WScript.Quit 1` を適切に使い分けること。監視システムは、スクリプトの中身を見るのではない。「戻り値(Exit Code)」 を見て、死んでいるのか生きているのかを判断するのだ。
結びに
自動化とは、単に作業を減らすことではない。「人間が介在すべきでない判断を、機械に、かつ確実に、ミスなく行わせること」 である。
FSOを使いこなすということは、オブジェクトの挙動、メモリの消費、そしてエラーの発生確率をコントロール下に置くということだ。次に君がコードを書くときは、必ず「もしこのファイルが巨大だったら?」「もしこのファイルがロックされていたら?」と自問自答してほしい。
それが、プロフェッショナルへの第一歩だ。
