◆イベントログ − VC++ 2005 その4 〜 イベントログの読込み
[ ホーム ] [ 上へ ] [ イベントログ - VC++ 2005 その5 〜 イベントログバックアップ&クリア と モニタ ]
<2.イベントログの検索 & 読込み>
(OpenEventLog、ReadEventLog関数)
イベントログの読み込みができれば後は任意で検索ができる ので、このセクションは読み込み中心になります。ReportEventでログを記録したら必要に応じて 閲覧したいですが、これはReadEventLogを使ってログを読み込みます。イベントビューワも このAPIを使っているはずです。つまり、イベントログの読込み≒イベントビューワの作成なのですが、 イベントビューワのようにGUIにするとコーディングが複雑になるので、今回はコンソールで アプリケーションログを出力してみたいと思います。もし、イベントビューワのようなものを 作成したかったら、これを使えばGUIの部分だけ実装すればできるはずです。
イベントログを読み込む際に主に使われるOpenEventLog、ReadEventLogの定義は次のとおりです。
>HANDLE OpenEventLog( LPCTSTR lpUNCServerName, // サーバー名へのポインタ LPCTSTR lpSourceName // ログファイルの名前へのポインタ );
BOOL ReadEventLog( HANDLE hEventLog, // イベントログのハンドル DWORD dwReadFlags, // ログの読み取り方法 DWORD dwRecordOffset, // 先頭レコードのオフセット LPVOID lpBuffer, // 読み取ったデータを受け取るバッファ DWORD nNumberOfBytesToRead, // 読み取るバイト数 DWORD *pnBytesRead, // 読み取ったバイト数 DWORD *pnMinNumberOfBytesNeeded // 必要なバイト数 );
OpenEventLogの第二引数にログファイル名を指定して開いたイベントログへの ハンドルをReadEventLogの第一引数に設定してログを読み取ります。ログファイル名は レジストリのイベントログキー のPrimaryModuleエントリに設定された名前のことです。今回はApplicationログに書き込んで いるのでOpenEventLogの第二引数は「Application」を指定して読み取ります。
ReadEventLogの第二〜第七引数までそれぞれ必要な値を指定しますがそれは次のとおりです。
(ReadEventLogの引数)
引数 | 内容 | |
hEventLog | OpenEventLogでオープンしたイベントログへのハンドルを 指定します。 | |
dwReadFlags | イベントログの読み取り方法を指定します。 | |
EVENTLOG_SEEK_READ | dwRecordOffsetで指定したレコード番号から読み取ります。 | |
EVENTLOG_SEQUENCIAL_READ | 最初から読み取り、最後に発行したReadEventLogで読み取った 次の位置から読み取ります。 | |
EVENTLOG_FORWARDS_READ | 古い順にログを読み取ります。 | |
EVENTLOG_BACKWORDS | 新しい順にログを読み取ります。 | |
dwRecordOffset | dwReadFlagにEVENTLOG_SEEK_READを指定した場合有効。 | |
lpBuffer | EVENTLOGRECORD構造体の配列の先頭位置を取得 (EVENTLOGRECORD構造体参照) |
|
nNumberOfBytesToRead | lpBufferに受取ることができるサイズ。lpBufferには EVENTLOGRECORD単位で結果が出力され、このサイズにあまりがでても、 EVENTLOGRECORDの途中まで出力されることはありません。 | |
pnBytesRead | 読み取ったバイト数が出力される( <= nNumberOfBytesToRead ) | |
pnMinNumberOfBytesNeeded |
次のイベントを受取るのに必要なバイト数が出力される (この関数が0を返し、GetLastErrorがERROR_INSUFFICIENT_BUFFERを返したときのみ有効) |
ReadEventLogを使ってEVENTLOGRECORDの配列を受取る方法は
lpBufferに結果であるEVENTLOGERCORD構造体の配列を受取り、実際に受取ったバイト数を *pnBytesReadで受取ります。そして、dwReadFlagsにEVENTLOG_SEQUENCILA_READを指定した場合は 次の読み取り位置を次に読み取るべき位置に移動します。
ということで、EVENTLOGRECORDを受取る適当なサイズのバッファを設定し、 dwReadFlagsにEVENTLOG_SEQUENCILA_READを指定して、0を返すまでReadEventLogを繰り返し 呼び出せばすべての内容を出力することができます。
また、任意のイベントを出力したい場合は、GetOldestEventLogRecordを 発行して最古のレコード番号を取得した上で、それより大きい番号をdwRecordOffsetで指定して、 EVENTLOG_SEEK_REEDでReadEventLogを発行することになります。 (ただし、GetNumberOfEventLogRecordsで得たレコード数を超えないこと)
次に受取ったEVENTLOGRECORD構造体の処理などについて解説します。
(EVENTLOGRECORD構造体)
目的のイベントログの各内容は、第四引数のlpBufferに EVENTLOGRECORD構造体の配列という形で受取ります。この構造体の中身は次のとおりですが、 基本的には各メンバを表示していけば、それでプチイベントビューワになります。
typedef struct _EVENTLOGRECORD { //この構造体に続く各データ(SIDや挿入文字列など)を含んだすべてのサイズ DWORD Length; //予約済み DWORD Reserved; //イベントログ内の絶対レコード番号(GetOldestEventLogRecordで取得する値) DWORD RecordNumber; //イベントが生成された時間(1970/1/1 0:0:0からの秒数) DWORD TimeGenerated; //イベントをイベントログサービスが受取った時間(1970/1/1 0:0:0からの秒数) DWORD TimeWritten; //イベントID(メッセージコンパイラで生成されたMyAppMsg.h内の32ビットの値) DWORD EventID; //イベントの種類(ReportEventの第二引数で指定した値) WORD EventType; //イベントIDに対応する説明文に挿入される文字列パラメータ(%1、%2・・・)の数 WORD NumStrings; //分類ID(メッセージコンパイラで生成されたMyAppMsg.h内の32ビットの値) WORD EventCategory; //予約済み WORD ReservedFlags; //予約済み DWORD ClosingRecordNumber; //イベントIDに対応する説明文に挿入する最初の文字列の位置 //(この構造体の先頭からのバイト単位の距離) DWORD StringOffset; //ユーザ名に対応するセキュリティ識別子(SID)の長さ DWORD UserSidLength; //セキュリティ識別子の開始位置(この構造体の先頭からのバイト単位の距離) DWORD UserSidOffset; //ReportEventの第九引数に指定して記録したアプリケーション固有のデータのサイズ DWORD DataLength; //アプリケーション固有のデータの開始位置 (この構造体の先頭からのバイト単位の距離) DWORD DataOffset; } EVENTLOGRECORD, *PEVENTLOGRECORD;
ここで注目したいのはStringOffset、UserSidOffset、DataOffsetです。実は 各EVENTLOGRECORDに続いて、説明文の挿入文字列、SID、固有データなどが収められていて、 上記のメンバはEVENTLOGRECORDの先頭からの開始位置までのバイト数を表しています。 1つのイベントに対する情報はEVENTLOGRECORD + αであり、可変長となるので、 EVENTLOGRECORDの最初のメンバLengthにこれらすべての情報を含めたバイト数が収められています。 このLengthの値はイベント情報の一番最後にも収められています。1つのイベントに対する EVENTLOGRECORDを含めたすべての情報は以下のようになっています。
(1イベント情報の構造)
(1) EVENTLOGRECORD::UserSidLength (2) EVENTLOGRECORD::DataLength (3) EVENTLOGRECORD::UserSidOffset (4) EVENTLOGRECORD::StringOffset (5) EVENTLOGRECORD::DataOffset
1つのイベント情報は上記のようになっているのでReadEventlogでイベント情報を 読み取ったらEVENTLOGRECORDを中心にポインタ演算をするなどして 各イベント情報を得ることになります。 日時やイベントソース名などはEVENTLOGRECORDが取得できれば比較的楽に得られますが、 問題なのはユーザ名、分類名(カテゴリ)、イベントIDに対応する説明文の取得です。
ユーザ名はセキュリティ識別子(SID)から変換しなければなりません。 つまり、イベントログへの書込みと反対の作業を行うことになりますが、 これはLookupAccountSidを使います。(以下の例のGetUserFromSID関数)
分類名と説明文はさらにややこしくて、
詳しくは以下の例のGetEventMessage関数をご覧下さい。
前置きが長くなりましたが、次にApplicationログ内のイベント一覧
(任意のイベントソースに対するイベント一覧)を出力してみます。
(例)
前々頁でイベントソースをレジストリに登録する際に
コンソールアプリケーションの
TestEventLogプロジェクトを作ったので、このプロジェクトを使ってイベントログの
読込み&表示を行います。
プロジェクトエクスプローラを表示してTestEventLogプロジェクトを選択後、
ツールバーのプロジェクト(P)→プロパティ(P)でプロパティページダイアログを表示してください。
左ペインで構成プロパティ + 全般
を選択して
右ペインのプロジェクトの既定値内の文字セットを
「マルチバイト文字セットを使用する」に変更してください。
TestEventLog.cppに以下のように加筆&修正してください。
// TestEventLog.cpp : コンソールアプリケーションのエントリポイントを定義します。 // #include "stdafx.h" #include <time.h> //レジストリへのイベントソースの書込み------------------------------------------------- int CreateEventSource(LPCTSTR lpszEventSrc, //イベントソース名 LPCTSTR lpszMsgDll, //イベントメッセージが登録されているDLL DWORD dwNumCategory); //カテゴリ数 //イベントログ読込み------------------------------------------------------------------- BOOL ListAppEventlog(LPCTSTR lpszEventSource); //アプリケーションイベントログ一覧表示 LPCTSTR GetEventType(WORD wEvtType); //イベントタイプ名 void GetEventMessage(LPCTSTR lpszEventSource, //メッセージIDに対応する分類名・説明取得 const WORD wCategory, LPTSTR *lppszCategory, const DWORD dwMsgId, LPTSTR *lppszMsg, const WORD NumStrings, LPCTSTR lpszStartString); BOOL GetUserFromSID(PSID pSid, LPTSTR *lppszUserName); //SIDに対応するユーザ名取得 //------------------------------------------------------------------------------------- int _tmain(int argc, _TCHAR* argv[]) { ListAppEventlog(argc <= 1 ? NULL : argv[1]); return ::_tsystem(_T("pause")); //return ! CreateEventSource(_T("MyApp"), _T("%SystemRoot%\\System32\\MyApp.dll"), 2); } //イベントソースのレジストリへの登録 //引数 lpszEventSrc イベントソース名 // lpszMsgDll イベントメッセージDLLへのフルパス // dwNumCategory イベントのカテゴリの種類数 //戻値 成功 1 失敗 0 int CreateEventSource(LPCTSTR lpszEventSrc, LPCTSTR lpszMsgDll, DWORD dwNumCategory) { // ・ // ・ // ・ } //------------------------------------------------------------------------------------- //アプリケーションイベントログ一覧表示 //引数 lpszEventSource 一覧表示するイベントソース // (NULLの場合はすべてのイベントログを出力) BOOL ListAppEventlog(LPCTSTR lpszEventSource) { HANDLE hEvent = 0; DWORD dwBufSize = 0; const DWORD BUF_SIZE = sizeof(EVENTLOGRECORD) * 1024; BYTE buf[BUF_SIZE]; DWORD dwGetBytes = 0; DWORD dwNeedBytes = 0; hEvent = ::OpenEventLog( NULL, _T("Application") ); if (! hEvent) return FALSE; //イベントログ読込み while ( ::ReadEventLog(hEvent, EVENTLOG_SEQUENTIAL_READ | EVENTLOG_FORWARDS_READ, 0, buf, BUF_SIZE, &dwGetBytes, &dwNeedBytes) ) { EVENTLOGRECORD *pEvtRec = reinterpret_cast<EVENTLOGRECORD *>(buf); while (dwGetBytes > 0) //ReadEventLogで読込んだ分だけループ { //イベントログリストアップ LPCTSTR lpszEvtSrc = (LPCTSTR) //イベントソース名 ( (PBYTE)pEvtRec + sizeof(EVENTLOGRECORD) ); const DWORD DATE_SIZE = 26; //日時格納用領域サイズ TCHAR szDateTime[ DATE_SIZE ]; time_t t = pEvtRec->TimeGenerated; //イベント日時 LPTSTR lpszCategory = NULL; //分類名 LPTSTR lpszMsg = NULL; //イベント説明 LPTSTR lpszUser = NULL; //ユーザ名 LPCTSTR lpszComputer = (LPCTSTR)((PBYTE)lpszEvtSrc //コンピュータ名 + (lstrlen(lpszEvtSrc) + 1) * sizeof(TCHAR)); //出力イベントログフィルタ if (lpszEventSource && _tcsicmp(lpszEventSource, lpszEvtSrc) != 0) { //次のEVENTLOGRECORD構造体へ移動 dwGetBytes -= pEvtRec->Length; pEvtRec = (EVENTLOGRECORD *)( (PBYTE)pEvtRec + pEvtRec->Length ); continue; } //szDataTimeにイベント日時取得 _tctime_s(szDateTime, DATE_SIZE, &t); //pszCategory, lpszMsgに分類名, 説明を取得 GetEventMessage(lpszEvtSrc, pEvtRec->EventCategory, &lpszCategory, pEvtRec->EventID, &lpszMsg, pEvtRec->NumStrings, (LPCTSTR)( (PBYTE)pEvtRec + pEvtRec->StringOffset ) ); //lpszUserにユーザ名取得 if (! pEvtRec->UserSidLength || ! GetUserFromSID((PSID)((PBYTE)pEvtRec + pEvtRec->UserSidOffset), &lpszUser) ) { lpszUser = new TCHAR[5]; ::_tcscpy_s(lpszUser, 5, _T("N/A")); //ユーザ名の記録がない時 } //イベントログ出力 _tprintf(_T("レコード番号: %d\n"), pEvtRec->RecordNumber); _tprintf(_T("日時 : %s" ), szDateTime); _tprintf(_T("ソース : %s\n"), lpszEvtSrc); _tprintf(_T("種類 : %s\n"), GetEventType(pEvtRec->EventType) ); _tprintf(_T("分類 : %s\n"), lpszCategory ? lpszCategory : _T("なし")); _tprintf(_T("イベントID : %d\n"), pEvtRec->EventID & 0x0000FFFF); _tprintf(_T("ユーザ : %s\n"), lpszUser); _tprintf(_T("コンピュータ: %s\n"), lpszComputer); if (lpszMsg) //説明取得成功 { _tprintf(_T("説明 : %s\n"), lpszMsg); } else //説明取得失敗 { _tprintf(_T("説明 : イベントIDに対応する説明が見つかりません。")); if (! pEvtRec->NumStrings) //挿入文字列なし { _tprintf(_T("\n")); } else //挿入文字列あり { _tprintf(_T("次の情報はイベントの一部です。\n")); LPCTSTR pszWalkInst = (LPCTSTR)((PBYTE)pEvtRec + pEvtRec->StringOffset); for (WORD i=0; i<pEvtRec->NumStrings; i++) { _tprintf(_T(" %s\n"), pszWalkInst); pszWalkInst = (LPCTSTR)( (PBYTE)pszWalkInst + (lstrlen(pszWalkInst) + 1) * sizeof(TCHAR) ); } } } _tprintf(_T("データ : ")); for (DWORD i=0; i<pEvtRec->DataLength; i++) { _tprintf(_T(" %02x"), *((PBYTE)pEvtRec + pEvtRec->DataOffset + i) ); } _tprintf(_T("\n-----------------------------------------------------------\n")); if (lpszUser) delete [] lpszUser; if (lpszCategory) delete [] lpszCategory; if (lpszMsg) delete [] lpszMsg; //次のEVENTLOGRECORD構造体へ移動 dwGetBytes -= pEvtRec->Length; pEvtRec = (EVENTLOGRECORD *)( (PBYTE)pEvtRec + pEvtRec->Length ); } } ::CloseEventLog(hEvent); hEvent = 0; return TRUE; } //イベント種類の取得 //引数 イベント種類のシンボル名 //戻値 イベント種類名 LPCTSTR GetEventType(WORD wEvtType) { switch (wEvtType) { case EVENTLOG_ERROR_TYPE: return _T("エラー"); break; case EVENTLOG_WARNING_TYPE: return _T("警告"); break; case EVENTLOG_INFORMATION_TYPE: return _T("情報"); break; case EVENTLOG_AUDIT_SUCCESS: return _T("成功の監査"); break; case EVENTLOG_AUDIT_FAILURE: return _T("失敗の監査"); break; default: return _T(""); } } //分類ID・イベントIDに対応する分類名・説明取得 //引数 lpszEventSource イベントソース名 // wCategory 分類ID // lppszCategory wCategoryに対応する分類名を受取るポインタのアドレス(結果出力) // dwMsgId イベントメッセージID // lppszMsg dwMsgIdに対応する説明を受取るポインタのアドレス(結果出力) // NumStrings *lppszMsgに挿入される文字列の数 // lpszStartString *lppszMsgに挿入される最初の文字列の位置 //仕様 *lppszCategory, *lppszMsgに分類名・説明が取得できた場合は呼出側で // delete [] *lppszCategory, delete [] *lppszMsgで領域を開放すること // 失敗時は*lppszCategory, *lppszMsgはNULLに設定される void GetEventMessage(LPCTSTR lpszEventSource, const WORD wCategory, LPTSTR *lppszCategory, const DWORD dwMsgId, LPTSTR *lppszMsg, const WORD NumStrings, LPCTSTR lpszStartString) { //イベントソースレジストリキーへのハンドル HKEY hReg = 0; TCHAR szSrcKey[MAX_PATH] = _T("SYSTEM\\CurrentControlSet\\Services\\Eventlog\\Application\\"); //CategoryMessageFile, EventMessageFileエントリサイズ DWORD dwEntrySize = 0; //CategoryMessageFile, EventMessageFileエントリの値 BYTE *pBufDll = NULL; //CategoryMessageFile, EventMessageFileファイルパス TCHAR szDllPath[MAX_PATH]; //CategoryMessageFile, EventMessageFileモジュールハンドル HMODULE hDll = NULL; //挿入文字列へのポインタ LPCTSTR *ppInstStr = NULL; //挿入文字列の配列の検索用 LPCTSTR pszWalkStr = NULL; //分類名/説明受取り用 LPVOID pBufMsg = NULL; LPCTSTR pszRegEntry[] = {_T("CategoryMessageFile"), _T("EventMessageFile")}; DWORD dwId[] = {wCategory, dwMsgId}; //レジストリイベントソースパス作成 ::_tcscat_s(szSrcKey, MAX_PATH, lpszEventSource); for (int i=0; i<sizeof pszRegEntry / sizeof pszRegEntry[0]; i++) { BOOL bRet = FALSE; do { //イベントソースレジストリキーOpen if ( ::RegOpenKeyEx(HKEY_LOCAL_MACHINE, szSrcKey, 0, KEY_READ, &hReg) != ERROR_SUCCESS ) break; //CategoryMessageFile, EventMessageFileへのパスサイズ取得(dwEntrySize取得) if (::RegQueryValueEx(hReg, pszRegEntry[i], NULL, NULL, NULL, &dwEntrySize) != ERROR_SUCCESS ) break; pBufDll = new BYTE[dwEntrySize]; //CategoryMessageFile, EventMessageFileへのパス取得(pBufDll取得) if ( ::RegQueryValueEx(hReg, pszRegEntry[i], NULL, NULL, pBufDll, &dwEntrySize) != ERROR_SUCCESS ) break; //環境変数文字列の展開(%SystemRoot%などを変換) if (! ::ExpandEnvironmentStrings((LPCTSTR)pBufDll, szDllPath, MAX_PATH) ) break; //分類メッセージファイル/メッセージファイルLoad hDll = ::LoadLibrary(szDllPath); if (! hDll) break; //イベントメッセージへの挿入文字列の構築(ppInstStr構築) if (i == 1 && NumStrings > 0) { ppInstStr = new LPCTSTR[ NumStrings ]; pszWalkStr = lpszStartString; for (int j=0; j<NumStrings; j++) { ppInstStr[j] = pszWalkStr; pszWalkStr += lstrlen(pszWalkStr) + 1; } } //イベントIDから分類名/イベントメッセージ作成 if (! ::FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_ARGUMENT_ARRAY | FORMAT_MESSAGE_MAX_WIDTH_MASK, hDll, dwId[i], MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&pBufMsg, 0, (va_list *)ppInstStr) ) break; bRet = TRUE; } while(0); if (bRet) //メッセージ構築成功 { DWORD dwSize = lstrlen( (LPCTSTR)pBufMsg ) + 1; LPTSTR lpszResult = new TCHAR[dwSize]; _tcscpy_s(lpszResult, dwSize, (LPCTSTR)pBufMsg); !i ? *lppszCategory = lpszResult : *lppszMsg = lpszResult; } else //メッセージ構築失敗 { !i ? *lppszCategory = NULL : *lppszMsg = NULL; } if (pBufMsg) {LocalFree(pBufMsg); pBufMsg = NULL;} if (ppInstStr) {delete [] ppInstStr; ppInstStr = NULL;} if (hDll) {::FreeLibrary(hDll); hDll = 0;} if (pBufDll) {delete [] pBufDll; pBufDll = NULL;} if (hReg) {::RegCloseKey(hReg); hReg = 0;} } //for ループ } //セキュリティ識別子に対応するユーザ名取得 //引数 pSid 取得したいユーザ名のセキュリティ識別子へのポインタ // lppszUserName 取得するユーザ名領域へのポインタのアドレス //仕様 成功した場合は*lppszUserNameにユーザ名が収められるので使い終わったら // 呼出側でdelete [] *lppszUserNameを実行すること // 失敗時は*lppszUserNameはNULLに設定される BOOL GetUserFromSID(PSID pSid, LPTSTR *lppszUserName) { BOOL bRet = FALSE; DWORD dwUserSize = 0; DWORD dwDomainSize = 0; LPTSTR pUser = NULL; SID_NAME_USE snu; //ユーザ名のサイズ取得 ::LookupAccountSid(NULL, pSid, NULL, &dwUserSize, NULL, &dwDomainSize, NULL); if (::GetLastError() != ERROR_INSUFFICIENT_BUFFER) return bRet; //ユーザ名領域確保 pUser = new TCHAR[dwUserSize]; //ユーザ名取得 bRet = ::LookupAccountSid(NULL, pSid, pUser, &dwUserSize, NULL, &dwDomainSize, &snu); if (bRet) { *lppszUserName = pUser; } else { delete [] pUser; pUser = NULL; *lppszUserName = NULL; } return bRet; } |
以上を記述したら、TestEventLogをビルドしたのち、コマンドプロンプトから
C:\>TestEventLog.exe MyApp [リターン]
を実行してみてください。
こんな感じで出力されます。