Einen Eintrag in die Ereignisanzeige schreiben


Die Ereignisanzeige

Mit Windows NT hat Microsoft die sogenannte Ereignisanzeige, oder auf englisch das EventLog, eingeführt. Damit haben sie für Anwendungen eine zentrale Möglichkeit geschaffen Log-Einträge abzulegen. Ein Administrator muss dann also nicht immer in sämtlichen Anwendungsverzeichnissen nach Logdateien suchen, um zum Beispiel Fehler zu diagnostizieren. Allerdings ist es für den Programmierer nicht ganz einfach dort einen Eintrag hinzuzufügen, was am Aufbau der Ereignisanzeige liegt. Microsoft hat die Ereignisanzeige so entworfen, dass es möglich ist, die Nachrichten möglichst einfach zu lokalisieren, also mehrsprachig zu gestalten. Deshalb liegen die Texte als Zeichenkettenressourcen in DLLs oder in der Exe selber vor. Eingetragen wird dann in das EventLog nur die ID der Nachricht. In der Registry wird dann dazu hinterlegt in welchen Modul der dazugehörige Text zu finden ist. Dies spart zu dem Speicherplatz im EventLog selber.

Im Einzelnen müssen also folgende Schritte gemacht werden, um mit der API Funktion ReportEvent einen Eintrag in die Ereignisanzeige zu schreiben:

  1. Erzeugen einer Ressourcendatei mit den Kategorien (optional) und den Texten.
  2. Kompilieren eines Moduls, welches diese Ressourcen enthält. Dies kann eine eigene DLL sein oder aber auch die Anwendung selber.
  3. Erzeugen eines Registryeintrages, in welchen Modul die Ereignisanzeige die Texte findet.
  4. Aufruf der API-Funktion, um einen Eintrag in der Ereignisanzeige zu machen.

Erzeugen der Ressourcendatei

Die Vorlage für die Ressourcendatei ist eine einfache Textdatei, die nur einem bestimmten Aufbau haben muss. Die Ressourcendatei kann sowohl die Kategorien als die Nachrichtentexte definieren. Man kann aber auch für beides getrennte Dateien anlegen und in getrennten Modulen ablegen. Ich werde sie beide zusammen in eine Datei schrieben und in einem Modul (der Anwendung selber) ablegen.

Beispielhafter Aufbau einer Ressourcendatei:

LanguageNames=(German=0x407:MSG00407)

MessageId=0x1
SymbolicName=CAT_1
Language=German
Kategorie 1
.

MessageId=0x10
SymbolicName=MSG_DEMO_ENTRY
Language=German
Testeintrag von %1.
.

In der ersten Zeile legt man eine Alias für die verwendete Sprache in der Datei an. In unserem fall ist das Deutsch. Dann folgen die Einträge. Dabei wird kein Unterschied zwischen einer Kategorie und einem Nachrichtentext gemacht. Aber es muss sichergestellt sein, dass jeder Block eine eindeutige MessageID besitzt, die als Hexadezimalwert in C-Schreibweise angegeben werden muss. Der Eintrag SymbolicName ist optional und dient nur dazu um für C automatisch eine Headerdatei anzulegen, wo die Kategorie dann als einfache Konstante abgelegt wird, um eine einfacheren Zugriff auf den Eintrag zu haben. Dann folgt die Sprache und zu letzt der Name der Kategorie bzw der Nachrichtentext. Abgeschlossen wird ein Block mit einem einzelnen Punkt in einer Zeile und einer abschließenden Leerzeile. Das ist wichtig, als nicht vergessen. Die "%1" ist nur ein Platzhalter, der später ausgefüllt wird.

Hat man dies erledigt, erzeugt man mit dem Microsoft Message Compiler (MC.exe, liegt dem PSDK bei.) eine Ressourcenvorlage, die man dann mit einem Ressourcen Compiler zu einer binären Ressourcendatei kompilieren kann. Um diese Schritte zu vereinfachen habe ich mir eine kleine Batch-Datei geschrieben:

@mc EvtLogMsg.mc
@brcc32 EvtLogMsg.rc -foEvtLogMsg.res
@pause

Die Ressourcenvorlage für die Nachrichten heißt EvtLogMsg.mc. Der Message Compiler von Microsoft und der Ressourcen Compiler von Borland müssen im Suchpfad liegen. Die so erzeugte Ressource kompilieren wir in die Exe mit ein oder erstellen eine separate DLL mit dieser Ressource.

Somit hätten wir Punkte eins und zwei aus unserer Liste abgehakt. Kommen wir zu Punkt drei:

Die Registryeinträge

Jedes Log (Anwendung, Sicherheit, System) enthält Untereinträge, sogenannte Quellen:

EventLog

Links sieht man die Standardlogs und rechts die Einträge in ausgewählten Log. Die Bezeichnung Quelle halte ich für etwas irre führend. In der Regel bezeichnet die Quelle die Anwendung, welche den Eintrag vorgenommen hat. Diese Quelle muss man in der Registry der Ereignisanzeige bekannt machen. Dies geschieht, in dem an einen Untereintrag im Schlüssel

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Eventlog\Application

macht, will man eine Quelle dem Anwendungslog hinzufügen. Dieser Eintrag ist bei meiner Demo-Anwendung "Demo". In diesem Schlüssel müssen wir noch vier Werte hinzufügen:

SchlüsselDatentypBedeutung
CategoryCountDWORDAnzahl der verfügbaren Kategorien
TypesSupportedDWORDNachrichtentypen die geloggt werden sollen
EventMessageFileZeichenfolgePfad zu dem Modul mit den Nachrichten
CategoryMessageFileZeichenfolgePfad zu dem Modul mit den Kategorien

Die Ereignisanzeige findet jetzt also die Quelle "Demo", daraufhin guckt sie in der Registry, ob eine entsprechende Quelle dort eingetragen ist. Ist sie das, kann die Ereignisanzeige aus dem dort angegebenen Modul die zu den MessageIDs gehörenden Texte anzeigen und den Platzhalter durch den entsprechenden Wert, der auch in der Ereignisanzeige abgelegt wurde, ersetzen.

Für die Demo-Anwendung habe ich den Schlüssel mal exportiert und der Demo beigelegt, so dass man die Eintrage für die Demo nicht von Hand machen muss:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Eventlog\Application\Demo]
"CategoryCount"=dword:00000001
"TypesSupported"=dword:00000000
"EventMessageFile"="E:\\Delphi\\Sourcecodes\\In_Bearbeitung\\MpuWriteEventLog\\Project1.exe"
"CategoryMessageFile"="E:\\Delphi\\Sourcecodes\\In_Bearbeitung\\MpuWriteEventLog\\Project1.exe"

Natürlich müssen die Pfade sehr wahrscheinlich angepasst werden, je nach dem wohin das Archiv mit der Demo entpackt wurde. Wird kein Eintrag in der Registry gefunden oder wurde das angegeben Modul nicht gefunden, wird ein Standardtext angezeigt, der in etwa so aussehen kann:

Die Beschreibung der Ereigniskennung ( 256 ) in ( Demo ) wurde nicht gefunden. Der lokale Computer verfügt nicht über die zum Anzeigen der Meldungen von einem Remotecomputer erforderlichen Registrierungsinformationen oder DLL-Meldungsdateien. Möglicherweise müssen Sie das Flag /AUXSOURCE= zum Ermitteln der Beschreibung verwenden. Weitere Informationen stehen in Hilfe und Support. Ereignisinformationen: Hallo.

Damit hätten wir auch Schritt drei erledigt. Kommen wir zum letzten Schritt:

Aufruf der API-Funktion

Das Eintragen erfolgt in den Windows üblichen Schritten:

  1. Ressource über das Handle identifizieren
  2. Handle auf Gültigkeit prüfen
  3. Schreiben / Lesen
  4. Handle wieder schließen

Diese vier Schritte muss man unter Windows immer machen, will man auf eine Ressource zugreifen - sei es nun eine Datei, die Registry, eine Zeichenfläche oder wie hier die Ereignisanzeige. Damit ist der zu schreibenden Code eigentlich schon fast geschrieben:

function LogMessage(Machine: WideString; EventType, CatID: Word; MsgID: Cardinal; Parameter1: WideString): DWORD;
var
  err: DWORD;
  hEventLog         : THandle;
  pmsgArray         : array[0..0] of PWideChar;
begin
  err := 0;
  hEventLog := OpenEventLogW(PWideChar(Machine), PWideChar(SOURCE));
  if hEventLog > 0 then
  begin
    pmsgArray[0] := PWideChar(Parameter1);
    if not ReportEventW(hEventLog, EventType, CatID, MsgID, nil, 1, 0, @pmsgArray, nil) then
      err := GetLastError;
    CloseEventLog(hEventLog);
  end
  else
    err := GetLastError;
  result := err;
end;

Mit OpenEventLog öffnen wir die Ereignisanzeige und erhalten ein Handle zurück. Mit Hilfe dieses Handles können wir dann mit ReportEvent einen Eintrag vornehmen. Der erste Parameter bezeichnet den Computer, dessen Ereignisanzeige geöffnet werden soll. Ist er nil, wird die lokale Ereignisanzeige geöffnet. Der zweite Parameter gibt unsere Quelle an. Diese muss identisch mit unserem Eintrag in der Registry sein, da wie schon gesagt, darüber die Zuordnung von der MessageID, die in der Ereignisanzeige gespeichert wird und dem Nachrichtentext aus dem Modul mit der Ressource erfolgt.

Der eigentliche Eintrag wird dann mit der API-Funktion ReportEvent gemacht:

BOOL ReportEvent(
  HANDLE hEventLog,
  WORD wType,
  WORD wCategory,
  DWORD dwEventID,
  PSID lpUserSid,
  WORD wNumStrings,
  DWORD dwDataSize,
  LPCTSTR* lpStrings,
  LPVOID lpRawData
);

Die für uns interessanten Parameter:

ParameterBedeutung
hEventLogHandle der Ereignisanzeige
wTypeTyp des Eintrages. Zur Verfügung stehen folgende Typen:
EVENTLOG_SUCCESS – Information event
EVENTLOG_AUDIT_FAILURE - Failure Audit event
EVENTLOG_AUDIT_SUCCESS - Success Audit event
EVENTLOG_ERROR_TYPE - Error event
EVENTLOG_INFORMATION_TYPE - Information event
EVENTLOG_WARNING_TYPE - Warning event
wCategoryID einer Kategorien aus der Ressource
dwEventIDID der Nachricht aus der Ressource
lpUserSIDSID des Benutzers
wNumStringsAnzahl der Zeichenfolgen in dem Array mit den Werten für die Platzhalter
lpStringsZeiger auf das Zeichenfolgen-Array mit den Werten für die Platzhalter

Mit dem Parameter lpRawData kann man noch Binäredaten in der Ereignisanzeige ablegen.

Und hat alles geklappt, dann sollte folgender Eintrag erzeugt worden sein:

Ereignisanzeigedetail

Demoprogramm


Demo_ReportEvent.zip Wednesday, 29-Dec-2010 23:45:19 CET 197K

2010-12-29T23:44:54 +0100, mail+homepage[at]michael-puff.de