Writing to the Windows Event Log using Delphi
Asked Answered
A

1

24

How can my Delphi app easily write to the Windows Event Log?

What is the difference between TEventLogger and ReportEvent? How do I use the ReportEvent function?

Aldo answered 14/5, 2015 at 5:24 Comment(3)
Searching Stack Overflow for this seemingly simple question returns answers spreaded between many questions. I've created a new simple question and spent time to combine the answers and add extra information that was not in the other answers. I've done that because it not the first time I came here looking for this answer and thought the detailed answer and sample project might help other people as well.Aldo
You could have done that at the other question. It's fine here too. The questions are linked now. It's all good. blog.stackoverflow.com/2010/11/…Alane
Ah ok, thanks David, I now understand better how it works.Aldo
A
39

If you are writing a Windows Service and need to write to the local machine's Windows Event Log then you can call TService.LogMessage as mentioned here.

//TMyTestService = class(TService)

procedure TMyTestService.ServiceStart(Sender: TService; var Started: Boolean);
begin
  LogMessage('This is an error.');
  LogMessage('This is another error.', EVENTLOG_ERROR_TYPE);
  LogMessage('This is information.', EVENTLOG_INFORMATION_TYPE);
  LogMessage('This is a warning.', EVENTLOG_WARNING_TYPE);
end;

For any other type of applications you can use the SvcMgr.TEventLogger undocumented helper class for TService to write the the local machine's Windows Event Log as mentioned here, here and here.

uses
  SvcMgr;

procedure TForm1.EventLoggerExampleButtonClick(Sender: TObject);
begin
  with TEventLogger.Create('My Test App Name') do
  begin
    try
      LogMessage('This is an error.');
      LogMessage('This is another error.', EVENTLOG_ERROR_TYPE);
      LogMessage('This is information.', EVENTLOG_INFORMATION_TYPE);
      LogMessage('This is a warning.', EVENTLOG_WARNING_TYPE);
    finally
      Free;
    end;
  end;
end;

You can also use the Windows API ReportEvent function as mentioned here and here.

I've created a simple class to make it easier, it is available on GitHub.

//----------------- EXAMPLE USAGE: ---------------------------------

uses
  EventLog;

procedure TForm1.EventLogExampleButtonClick(Sender: TObject);
begin
  TEventLog.Source := 'My Test App Name';

  TEventLog.WriteError('This is an error.');
  TEventLog.WriteInfo('This is information.');
  TEventLog.WriteWarning('This is a warning.');
end;

//------------------------------------------------------------------

unit EventLog;

interface

type
  TEventLog = class
  private
    class procedure CheckEventLogHandle;
    class procedure Write(AEntryType: Word; AEventId: Cardinal; AMessage: string); static;
  public
    class var Source: string;
    class destructor Destroy;

    class procedure WriteInfo(AMessage: string); static;
    class procedure WriteWarning(AMessage: string); static;
    class procedure WriteError(AMessage: string); static;

    class procedure AddEventSourceToRegistry; static;
  end;

threadvar EventLogHandle: THandle;

implementation

uses Windows, Registry, SysUtils;

class destructor TEventLog.Destroy;
begin
  if EventLogHandle > 0 then
  begin
    DeregisterEventSource(EventLogHandle);
  end;
end;

class procedure TEventLog.WriteInfo(AMessage: string);
begin
  Write(EVENTLOG_INFORMATION_TYPE, 2, AMessage);
end;

class procedure TEventLog.WriteWarning(AMessage: string);
begin
  Write(EVENTLOG_WARNING_TYPE, 3, AMessage);
end;

class procedure TEventLog.WriteError(AMessage: string);
begin
  Write(EVENTLOG_ERROR_TYPE, 4, AMessage);
end;

class procedure TEventLog.CheckEventLogHandle;
begin
  if EventLogHandle = 0 then
  begin
   EventLogHandle := RegisterEventSource(nil, PChar(Source));
  end;
  if EventLogHandle <= 0 then
  begin
    raise Exception.Create('Could not obtain Event Log handle.');
  end;
end;

class procedure TEventLog.Write(AEntryType: Word; AEventId: Cardinal; AMessage: string);
begin
  CheckEventLogHandle;
  ReportEvent(EventLogHandle, AEntryType, 0, AEventId, nil, 1, 0, @AMessage, nil);
end;

// This requires admin rights. Typically called once-off during the application's installation
class procedure TEventLog.AddEventSourceToRegistry;
var
  reg: TRegistry;
begin
  reg := TRegistry.Create;
  try
    reg.RootKey := HKEY_LOCAL_MACHINE;
    if reg.OpenKey('\SYSTEM\CurrentControlSet\Services\Eventlog\Application\' + Source, True) then
    begin
      reg.WriteString('EventMessageFile', ParamStr(0)); // The application exe's path
      reg.WriteInteger('TypesSupported', 7);
      reg.CloseKey;
    end
    else
    begin
      raise Exception.Create('Error updating the registry. This action requires administrative rights.');
    end;
  finally
    reg.Free;
  end;
end;

initialization

TEventLog.Source := 'My Application Name';

end.

ReportEvent supports writing a log entry to either a local or remote machine's Event Log. For a remote example see John Kaster's EDN article.


Note that you would also have to create a message file and register your event source otherwise all your log messages will be starting with something like this:

The description for Event ID xxx from source xxxx cannot be found. Either the component that raises this event is not installed on your local computer or the installation is corrupted. You can install or repair the component on the local computer.

If the event originated on another computer, the display information had to be saved with the event.

The following information was included with the event:

1, For more information on how to create a message file see Finn Tolderlund's tutorial or Michael Hex's article or you can use an existing MC and RES file included in the GitHub project.

2, Embed the RES file into your application by including the MessageFile.res in your DPR file. Alternatively you can create a dll for the messages.

program MyTestApp;

uses
  Forms,
  FormMain in 'FormMain.pas' {MainForm},
  EventLog in 'EventLog.pas';

{$R *.res}
{$R MessageFile\MessageFile.res}

begin
  Application.Initialize;

3, The once-off registration requires admin rights writing to the registry so it us usually done as part of your application's installation process.

//For example
AddEventSourceToRegistry('My Application Name', ParamStr(0));
//or
AddEventSourceToRegistry('My Application Name', 'C:\Program Files\MyApp\Messages.dll');

//--------------------------------------------------

procedure AddEventSourceToRegistry(ASource, AFilename: string);
var
  reg: TRegistry;
begin
  reg := TRegistry.Create;
  try
    reg.RootKey := HKEY_LOCAL_MACHINE;
    if reg.OpenKey('\SYSTEM\CurrentControlSet\Services\Eventlog\Application\' + ASource, True) then
    begin
      reg.WriteString('EventMessageFile', AFilename);
      reg.WriteInteger('TypesSupported', 7);
      reg.CloseKey;
    end
    else
    begin
      raise Exception.Create('Error updating the registry. This action requires administrative rights.');
    end;
  finally
    reg.Free;
  end;
end;

If you have need Windows event logging and other logging requirements you can also use logging frameworks such as log4d and TraceTool


See here if you want to write to the Event Log window in the Delphi IDE.

Aldo answered 14/5, 2015 at 5:24 Comment(10)
Well done! Just to your log class. I would prefer their methods to be the instance methods rather than class methods to avoid the repetitive registering and deregistering event source. I would register the event source when the instance of the class is created and deregister when destroyed. Or make a global threadvar and initialize it once.Alliterate
If you unregister when the instance is destroyed, does it not cause that existing event log entries can't be read anymore? In other words, the instance had to be alive for the operator to be able to view old event log entries? I rather think that registering the event source should be part of the installation and unregistering to the uninstallation of the executable.Unbelieving
Hi TOndrey, I suspect TLama meant the "RegisterEventSource" API call, not "Register the Event Source by adding it to the registry" :-)Aldo
Hi TLama, thanks for the suggestion. I initially thought that might have been micro-optimization and I liked the one line ease of use of the class methods :-)Aldo
I have changed it to use a global threadvar, not sure if it is 100% correct, please see github.com/Kobus-Smit/EventLogHelper/blob/master/EventLog.pas I've also added the instance methods version github.com/Kobus-Smit/EventLogHelper/blob/master/Test%20app/…Aldo
And included a quick benchmark comparison, it is +-3 times faster: Starting... 5000x TEventLogOld.WriteInfo: 553 ms 7918028 ticks 5000x TEventLogTest.WriteInfo: 196 ms 2812860 ticks 5000x TEventLog.WriteInfo: 194 ms 2789610 ticks 5000x Parallel TEventLogOld.WriteInfo: 361 ms 5177687 ticks 5000x Parallel TEventLogTest.WriteInfo: 164 ms 2352198 ticks 5000x Parallel TEventLog.WriteInfo: 147 ms 2105081 ticks Done.Aldo
This implementation works correctly, except for Windows 10. When running the code from the answer, this is logging correctly the message, but is faulting applications mmc.exe and consent.exe. A possible fix can be founded here - social.technet.microsoft.com/Forums/en-US/…. +1 for gathering all the resources on a single thread.Makassar
Your class on github has one problem: RegisterEventSource is called for multiple threads but DeregisterEventSource is only called once in main thread context. Probably you are leaking one Handle for each secondary thread which calls RegisterEventSource.Cannular
@KobusSmit, how can I properly write multi-param messages? I mean those that defined like "Task %1 completed with result %2" in the message file. Most of implementations take one string parameter. Can it be done with ReportEvent?Stepaniestepbrother
P.S. The answer for my question is "yes". ReportEvent can be used for logging parameterless messages as well as messages with insertings.Stepaniestepbrother

© 2022 - 2024 — McMap. All rights reserved.