Writing an event logger with Delphi 2010

By: John Kaster

Abstract: Use this free utility to write event logs to Windows PCs, and learn how to do it from the Delphi source project

In This Article

Tracking deployment versions

We use many Windows servers (and a few Linux servers) for the Developer Network, with most of our web applications and services running on multiple load balanced servers. (We also use the same set of javascript, css, image and template files to share as much of our look and feel as possible across different web applications.)

Because we have multiple servers running the same application, we need a convenient way to verify that the same version of the application is running on each server. If we have a problem with a specific server, our IT department also wants to be able to track the changes that have been made to that server to see if a recent change is the cause of the problem.

Fortunately, since we use automated tools to deploy our applications and any resources they use, we can log directly to the destination server exactly what changes are made in a deployment – provided we have a logging tool that allows us to write log entries to the remote servers.

The Windows Event Viewer

All recent Windows PCs (since Windows NT) have a Windows Event Viewer. Most IT departments, (and definitely the Embarcadero IT department) will refer to the event log to diagnose a problematic Windows server.

When a server that needs to be live 24x7 is having trouble, you want to be able to find out as quickly as possible what changes might have been made to that server to introduce an instabilities, or to eliminate the possibility that recent changes introduced the instability.

Having an easily accessible record of the changes deployed to each of our EDN servers was so desirable I wrote a logging tool in Delphi to do it. I named it ELog – short for "Event Log."

Writing a log entry

The windows API ReportEvent function is used to write a log entry to a machine. It supports writing a log entry to either a local or remote machine.

Writing on a local machine

The Windows API call is wrapped up into a Delphi routine called WriteEventLog. This routine is similar to one I found on Stack Overflow, but I also referred to SvcMgr.TEventLogger in the Delphi RTL to make sure I was writing the event log in a practical way. TEventLogger does not currently have a property for the remote server name. If it did, I would use it to write the log entry instead of the routine below.

uses Windows;
...
const
  SDefaultSource = 'ELog';
...
// TEventLogger is not used because the current RTL supports only local servers
function WriteEventLog(AEntry: string;
  AServer: string ='';
  ASource: string = SDefaultSource;
  AEventType: word = EVENTLOG_INFORMATION_TYPE;
  AEventId: word = 0;
  AEventCategory: word = 0
  ): boolean;
var
  EventLog: integer;
  P: Pointer;
begin
  Result := False;
  P := PWideChar(AEntry);
  if Length(AServer) = 0 then // Write to the local machine
    EventLog := RegisterEventSource(nil, PWideChar(ASource))
  else // Write to a remote machine
    EventLog := RegisterEventSource(PWideChar(AServer),
      PWideChar(ASource));
  if EventLog <> 0 then
  try
    ReportEvent(EventLog, // event log handle
          AEventType,     // event type
          AEventCategory, // category zero
          AEventId,       // event identifier
          nil,            // no user security identifier
          1,              // one substitution string
          0,              // no data
          @P,             // pointer to string array
          nil);           // pointer to data
    Result := True;
  finally
    DeregisterEventSource(EventLog);
  end;
end;

Writing on a remote machine

The account running ELog must have the appropriate rights for the remote machine to be able to write to it. If the active account has sufficient rights, you can provide the name of the remote server with UNC notation to write an entry to the remote log, such as "\\myserver".

Viewing the log entry

You can start your event viewer from Administrative Tools in the Windows Start menu.

When you view an entry created by WriteEventLog, you will see something similar to the following screen shot. (The actual display will vary among Windows operating systems.)

1An event log entry

That little red box is the actual text that was written to the event log. All that superfluous text obscuring your actual log entry is the Event Viewer telling you that it couldn't find a message resource file for your type of event. We'll need to register a message file to get rid of it all that fluff.

Using a message file for your event log

Message resource files are used to (potentially) localize a template for your exception event ids. You can look at this MSDN entry on message files for more background.

Creating the message file

The MSDN site describes how to create and register message files using various steps in Visual Studio. Fortunately, mc.exe is available in the freely available Windows SDK download. A good explanation of the steps you need to take for the .mc file can be found on Finn Tolderlund's Delphi Service application tutorial.

The ELog project has a build event that will automatically call the mc.exe to compile when the application is built.

Right mouse click on the project in the project explorer to bring up the options dialog. Click Edit… to edit the Pre-Build commands, which adds a blank Pre-Build event if none is selected. If mc.exe is in a different location for you, simply change the location.

The complete MessageFile.mc file is included in the download on CodeCentral. It is configured to simply display the contents of the event log entry for various event IDs. This excerpt shows the syntax:

MessageId=0x1
Severity=Success
Facility=Application
SymbolicName=CATEGORY_SUCCESS
Language=English
%1

Embedding the RES file

Once mc.exe builds the .rc file, Delphi can compile and embed the resource (.res) for us.

Building the project will create the resource file compatible with the Windows Event Viewer.

Here's the Delphi project code for ELog.dpr:

program ELog;

{$APPTYPE CONSOLE}

{$R 'MessageFile.res'}

uses
  SysUtils,
  ELogModule in 'ELogModule.pas' {LogModule: TLogModule},
  CommandParser in '..\CommandParser\CommandParser.pas',
  ParseIds in '..\CommandParser\ParseIds.pas',
  PropertyHelpers in '..\CommandParser\PropertyHelpers.pas';

var
  Log: TLogModule;

begin
  Log := nil;
  try
    try
      Log := TLogModule.Create(nil);
      Log.Execute;
    except
      on E: Exception do
      begin
        Writeln(E.ClassName, ': ', E.Message);
        Halt(1);
      end;
    end;
  finally
    FreeAndNil(Log);
  end;
end.

The important part for referencing our special message file resource is the {$R 'MessageFile.res'}.

Registering the message file

Once the message file is created and automatically embedded into our executable, we can register Elog as the message resource file. Registration is performed by writing some registry values into HKEY_LOCAL_MACHINE, as shown below.

const
  SEventLogKey = '\SYSTEM\CurrentControlSet\Services\Eventlog\Application\';

function RegisterMessageFile(
  ASource: string = SDefaultSource;
  AServer: string = '';
  AEventId: word = DefaultEventId;
  AMessageFile: string = ''): boolean;

var
  Reg: TRegistry;
  Key: string;

begin
  Result := False;

  Reg := TRegistry.Create(KEY_READ or KEY_WRITE);
  try
    Reg.RootKey := HKEY_LOCAL_MACHINE;
    if Length(AServer) > 0 then
      if not Reg.RegistryConnect(AServer) then
        raise ELogException.CreateFmt(StrNoRegistryConnection, [AServer]);
    Key := SEventLogKey + ASource;
    if Reg.OpenKey(Key, True) then
    begin
      if Length(AMessageFile) = 0 then
        AMessageFile := ParamStr(0); // Default to current application
      Reg.WriteString('EventMessageFile', AMessageFile); // do not translate
      Reg.WriteInteger('TypesSupported', 7); // do not translate
      Reg.CloseKey;
      Result := True;
    end;
  finally
    Reg.Free;
  end;
end;

Note that this routine can write to both the local machine's registry and to remote machine registries.

On a local machine

For registering the message file on a local machine, leave the value for AServer blank.

On a remote machine

The account running this code must have the appropriate rights for the remote machine to be able to write to its registry. If your active account has the rights, you can provide the name of the remote server with UNC notation to register the message file "\\myserver".

There is one issue with registering the message file on a remote machine: you either need to copy the file containing the message file resource to that machine, or put it somewhere the remote machine can read it.

Fortunately for me, all of our servers have the ability to see a share on our deployment server, so I'm able to configure the remote machine to use \\deployment\share\elog.exe if I choose. You may prefer to instead copy the file to a standard place on each remote machine and remotely register the message file using a path local to that machine

If you want to register ELog.exe (discussed further below) locally, you can just do:

ELog /r 

This will register the message file for ELog for an event source called ELog, with EventId 1.

Note: You don't have to register an Event Viewer message file on the remote machine. You can still write logs to that server, and view your log entries. They'll just have that superfluous text as shown in the sample screenshot above.

Creating the ELog application

The ELog utility is a Delphi console application. Nearly all of the logic it uses is contained in a DataModule, which makes it easier to develop and maintain.

Accepting command-line options

For a command-line utility, obviously a very important feature is an easy way to specify, parse, and verify the options passed in to ELog. I created the component TCommandParser exactly for this purpose. It is included with the Delphi 2010 (and Delphi 2009) sample application DbxDataPump, which has many command-line switches.

Using TCommandParser

Because the property and component editors for TCommandParser need some improvement, I typically instantiate it directly in Delphi code. This also allows me to easily instantiate it in a unit that doesn't have access to the designer. In ELog, the command parser is created on demand when the command-line options are first processed, and freed in the data module's destroy event.

Create properties for storing options

TCommandParser supports getting and setting switch values from published properties, which makes it very convenient for setting values via the command-line or configuration files (this is another reason I created a data module for ELog). This implementation makes it possible to put complex logic into your property setters, and do any sort of validation that might be required.

Here's a sample of the published property declarations in TLogModule:

  published
    property FileName: string read FFileName write SetFileName;
    property Server: string read FServer write FServer;
    property Source: string read FSource write SetSource;
    property Entry: string read FEntry write SetEntry;
    property EventType : word read FEventType write FEventType
      default EVENTLOG_INFORMATION_TYPE;
    property EventId: word read FEventId write FEventId default DefaultEventId;
    property Category: word read FCategory write FCategory
      default DefaultCategory;
    property QuietMode: boolean read FQuietMode write FQuietMode;
    property ShowHelp: boolean read FShowHelp write FShowHelp;
    property RegisterFile: boolean read GetRegisterFile write SetRegisterFile;
    property UnRegisterFile: boolean read GetUnregister write SetUnregister;
    property MessageFile: string read FMessageFile write FMessageFile;

As you can see from this code, some of the property getters and setters perform special (albeit simple) processing when encountering a command-line switch:

procedure TLogModule.SetFileName(const Value: string);
var
  Contents: TStringList;
begin
  FFileName := Value;
  if FileExists(FFileName) then
  begin
    Contents := TStringList.Create;
    try
      Contents.LoadFromFile(FFileName);
      Entry := Contents.Text;
    finally
      FreeAndNil(Contents);
    end;
  end
  else
    raise ELogException.CreateFmt(StrFileNotFound, [ FFileName]);
end;

procedure TLogModule.SetRegisterFile(const Value: boolean);
begin
  if Value then
    FRegAction := raRegister
  else
    FRegAction := raNone;
end;

procedure TLogModule.SetSource(const Value: string);
begin
  if SameText(Value, SBadSource) then
     raise ELogException.CreateFmt(StrReservedSource, [Value]);
  FSource := Value;
end;

procedure TLogModule.SetUnregister(const Value: boolean);
begin
  if Value then
    FRegAction := raUnregister
  else
    FRegAction := raNone;
end;

Registering switches

The command parser has a collection of command switches (options) that tell it how to process the command line, a configuration file, and any environment variable switches that might exist for the application. The class defining these command switches is called TCommandSwitch.

  TSwitchType = (stString, stInteger, stDate, stDateTime, stFloat, stBoolean);

  TCommandSwitch = class
    Name: string;
    Kind: TSwitchType;
    Required: boolean;
    Default: string;
    LongName: string;
    UniqueAt: integer;
    PropertyName: string;
    Description: string;
    Value: string;
    function SwitchSyntax(const ADefSwitch: char): string;
    function Syntax(const ADefSwitch, ADefArg: char): string;
    function Assigned: boolean;
    function Named: boolean;
    function Positional: boolean;
    function ParamName: string;
    function Optional: boolean;
    function HasDefault: boolean;
    function CurrentValue: string;
    function QuotedValue(AValue: string = ''): string;
    function HasValue: boolean;
    function HasProperty: boolean;
    procedure SetProperty(const AObject: TPersistent);
    function GetProperty(const AObject: TPersistent): string;
    procedure SetBoolean(const AObject: TPersistent; ATrueVals: string = '');
    constructor Create(const AName: string; const AKind: TSwitchType;
      const ARequired: boolean = False; const ADefault: string = '';
      const ADescription: string = ''; const ALongName: string = '';
      const APropertyName: string = '');
  end;

As you can see by examining the property setters I've included above, setting the property of some switches affects the value of another property (for example, setting the FileName property loads the contents of the file into the Entry property), so the order in which a switch is registered can be important. In this case, the Entry property's switch should be registered before the FileName property switch, as the following code shows.

procedure TLogModule.ParseOptions;
const
  YN: array[boolean] of string = ('off', 'on'); // do not translate
begin
  if not Assigned(FCmdParser) then
  begin
    FCmdParser := TCommandParser.Create(Self, false, StrEventLogger,
      'ELog'); // Do not translate
    FCmdParser.AddSwitch('e', stString, False, Entry, StrTextOfEntryToLog,
      'Entry', 'Entry'); // Do not translate
    FCmdParser.AddSwitch('f', stString, False, FileName, StrFileName,
      'File', 'FileName'); // Do not translate
    FCmdParser.AddSwitch('s', stString, False, Server, StrServerName,
      'Server', 'Server'); // Do not translate
    FCmdParser.AddSwitch('so', stString, False, SDefaultSource, StrEventSourceName,
     'Source', 'Source'); // Do not translate
    FCmdParser.AddSwitch('t', stInteger, False, IntToStr(EVENTLOG_INFORMATION_TYPE),
      StrEventType, 'Type', 'EventType'); // Do not translate
    FCmdParser.AddSwitch('i', stInteger, False, IntToStr(EventId), StrEventId,
      'Id', 'EventId'); // Do not translate
    FCmdParser.AddSwitch('c', stString, False, IntToStr(Category),
      StrEventCategory, 'Category', 'Category'); // Do not translate
    FCmdParser.AddSwitch('q', stBoolean, False, YN[True], // switch present = on
      StrQuietMode, 'Quiet', 'QuietMode');
    FCmdParser.AddSwitch('h', stBoolean, False, YN[True],  // switch present = on
      StrShowHelp, 'Help', 'ShowHelp'); // Do not translate
    FCmdParser.AddSwitch('r', stBoolean, False, YN[True], // switch present = on
      StrRegisterMessage, 'Register', 'RegisterFile');
    FCmdParser.AddSwitch('u', stBoolean, False, YN[True], // switch present = on
      StrUnregisterMessage, 'Unregister', 'UnregisterFile'); // Do not translate
    FCmdParser.AddSwitch('m', stString, False, MessageFile, StrEventSourceName,
     'MessageFile', 'MessageFile'); // Do not translate
  end;

  FCmdParser.ProcessCommandLine;
end;

If a Boolean switch has a default value (as the code above sets), specifying that switch without a Boolean value will set the switch to the default value. You can use +, true, t, yes, y, or on to toggle a Boolean switch to true, and -, false, f, n, no, off to set it to false to avoid ambiguities. If a Boolean switch has no default value and no passed value, the switch is ignored.

Processing the command line

The last line of the method listed above processes any switches provided to ELog, setting the values of the referenced properties accordingly. Once all the options are parsed, ELog can execute logic based on the configured values of the TLogModule properties.

procedure TLogModule.Execute;
begin
  if (Length(Entry) = 0) and (FRegAction = raNone) then
    WriteLn(CmdParser.Syntax)
  else if ShowHelp then
    WriteLn(CmdParser.HelpText)
  else if not QuietMode then
    WriteLn(CmdParser.Description);
  // Status('Switches: ' + CmdParser.Options); // use this to see switch values

  case FRegAction of
    raNone:
      if Length(Entry) > 0 then
      begin
        WriteEventLog;
        Status(Format(StrEventLogWritten, [MachineDesc]));
      end;
    raRegister:
      begin
        RegisterMessageFile;
        Status(Format(StrRegistrationAction,
          [ MessageFile, StrRegistered, Source, MachineDesc]));
      end;
    raUnregister:
      begin
        UnregisterMessageFile;
        Status(Format(StrRegistrationAction,
          [MessageFile, StrUnregistered, Source, MachineDesc]));
      end;
  end;
end;

There's a bit more to ELog than the code I've shown here, but since you can view all the code in the download yourself, I'll move on to actually using ELog.

Using ELog

We're now ready to discuss running ELog, after a small digression that explains how we plan to use ELog for EDN deployments.

Logging deployment changes with Robocopy

A very convenient tool we use for deploying updates is call Robocopy. Robocopy is included with Windows Vista and later, and can be installed with a resource kit for Windows Server 2003. Robocopy has this great option to mirror files (including subdirectories) from a source directory to a destination directory. Most importantly, these source and destination directories can also be on different machines. Rsync (from *nix operating systems) provides similar capabilities and also supports ftp, but it requires installation of a service on every destination server. (We don't need to use ftp deployment in our current environment.)

Delphi also has a very nice deployment tool available from the IDE, but there isn't a command-line version of it that is as configurable as Robocopy. Visual Studio also has a web deployment plug-in, but that deploys as part of the build process.

Finally, Robocopy supports logging what its operations to a log file. This log can be used to record the exact changes that were made to the destination device.

Because of its ready accessibility for on-demand deployment, highly configurable options, its ability to log its operations, and free availability for Windows, we have selected Robocopy as our preferred deployment tool.

We'll be using the log file it can create as the source of our log entry for ELog.

Command-line options

You can generate the command-line syntax supported by TCommandParser by calling its TCommandParser.Syntax method. A slightly more informative output is available from TCommandParser.HelpText. This is what TCommandParser.HelpText generates from invoking Elog without parameters, or by using the /h (or /help) switch:

Event Logger
Syntax:
  ELog.exe [/Entry| /e] [/File| /f] [/Server| /s] [/Source| /so] [/Type| /t] 
  [/Id| /i] [/Category| /c] [/Quiet| /q] [/Help| /h] [/Register| /r] 
  [/Unregister| /u] [/MessageFile| /m]
Parameters:
  [/Entry| /e] - Text of entry to log (Overridden by file switch) (Optional)
  [/File| /f] - Name of text file containing contents of entry to log (Optional)
  [/Server| /s] - Remote server name with UNC convention (omit for local machine) (Optional)
  [/Source| /so] - Event source name (Optional)
    Default: ELog. Example: /so:ELog
  [/Type| /t] - Event type, default is EVENTLOG_INFORMATION_TYPE (Optional)
    Default: 4. Example: /t:4
  [/Id| /i] - Event id (Optional)
    Default: 1. Example: /i:1
  [/Category| /c] - Event category (Optional)
    Default: 0. Example: /c:0
  [/Quiet| /q] - Quiet mode (Optional)
    Default: on. Example: /q:on
  [/Help| /h] - Show this help page (Optional)
    Default: on. Example: /h:on
  [/Register| /r] - Register a message file for the specified Source. (Optional)
    Default: on. Example: /r:on
  [/Unregister| /u] - Remove message file registration for the specified Source. (Optional)
    Default: on. Example: /u:on
  [/MessageFile| /m] - Event source name (Optional)
    Default: "d:\util\ELog.exe". Example: /m:"d:\util\ELog.exe"

Writing a log entry the local machine

You can write a log entry to the local machine with:

ELog /e='This is my ELog entry'

Use either single or double quotes for any switch value with whitespace or special characters.

Writing a log entry to a remote machine

You can write a log entry to the local machine with:

ELog /e='This is my ELog entry' /s=\\remote1

Registering the message file locally

You can register ELog's message file on the local machine with:

ELog /r

Registering the message file remotely

You can register ELog's message file for a shared source on a remote machine with:

ELog /r /m=\\sharedserver\shared\elog.exe /s=\\remote1

Backing up current deployments

Even though we have many automated tests in place to avoid new errors, launching a new version always has a chance of revealing a problem. Before deploying a new version, we also want to create a backup of the existing deployment, just in case we need to revert back to it.

We create a backup archive that includes the date and time of the backup. Two more tools are required for this: a utility batch file, and a file archiving tool.

Date and time variables for batch files

When we do a deployment, we also create a uniquely named backup of the currently deployed application on the target servers. Originally, I was going to write another Delphi command-line tool to provide date/time variables to batch files, but then I found Rob Vanderwoude's page on date and time variable processing in batch files.

A little while later, I had this datetime.cmd batch file:

@echo off
rem Good samples are available at http://www.robvanderwoude.com/datetimentparse.php
set show=echo
if "%1" == "q" set show=rem

%show% It's %Date% at %time% right now
FOR /F "tokens=1-4 delims=/- " %%A IN ("%Date%") DO (
    SET DOW=%%A
    SET Month=%%B
    SET Day=%%C
    SET Year=%%D
)

FOR /F "tokens=1-4 delims=:." %%A IN ("%Time%") DO (
    SET Hours=%%A
    SET Minutes=%%B
    SET Seconds=%%C
    SET Milliseconds=%%D
)
SET /A Hours = 100%Hours% %% 100
ECHO.%Minutes% | FIND /I "P" >NUL && SET /A Hours += 12
SET Minutes=%Minutes:~0,2%
SET /A Minutes = 100%Minutes% %% 100

set DTOS=%Year%%month%%day%
set TTOS=%Hours%%Minutes%%Seconds%.%Milliseconds%
set DateTime=%DTOS%.%TTOS%

%show% DOW=%DOW%
%show% Month=%Month%
%show% Day=%Day%
%show% Year=%Year%
%show% Hours=%Hours%
%show% Minutes=%Minutes%
%show% Seconds=%Seconds%
%show% Milliseconds=%Milliseconds%
%show% DTOS=%Year%.%month%.%day%
%show% TTOS=%TTOS%
%show% DateTime=%DateTime%

Running it will provide output similar to:

d:\>datetime
It's Tue 02/09/2010 at 11:38:28.13 right now
DOW=Tue
Month=02
Day=09
Year=2010
Hours=11
Minutes=38
Seconds=28
Milliseconds=13
DTOS=2010.02.09
TTOS=113828.13
DateTime=20100209.113828.13

For use inside other batch files, adding "q" as a parameter to the batch file will prevent it from echoing out the variables.

Creating a backup archive

7-zip is an excellent open source file archiving tool. It's available on a variety of platforms, and provides very good compression. (The Delphi installation files are compressed with 7-zip algorithms to minimize the install size.) 7-zip has a command-line interface that can be combined with the datetime utility listed above to create a uniquely named, date and time stamped backup file.

The following batch commands show an example of how this could be used:

call datetime q
7z a -t7z -r "d:\myapp%datetime%.7z" "d:\myapp\*.*" > deploy.log

This option will recurse through all directories of d:\myapp, using 7-zip's preferred compression method, and create an archive file named with the current date and time, including milliseconds. Using the example from the datetime batch file output above, the file name would be d:\myapp20100209.113828.13.7z. The actions taken by 7-zip will be recorded into deploy.log.

All together now

This sample batch file for deploying updates to the CodeCentral application combines ELog, datetime, and 7-zip to deploy from our master staging/test server to our production servers:

@echo off
setlocal ENABLEDELAYEDEXPANSION
call datetime q

set app=cc
del deploy%app%.*.log

set staging=ccmaster
set ecom=cc1 cc2 cc2

set euser=deployuser
set epass=deploypass

echo logging on to %staging% ...
net use \\%staging% /user:%euser% %epass%

for %%a in (%ecom%) do (
  echo logging on to %%a
  net use \\%%a /user:%euser% %epass%
  echo creating \\%%a\d\apps\%app%.%datetime%.7z ...
  7z a -t7z -r "\\%%a\d\apps\%app%.%datetime%.7z" "\\%%a\d\apps\%app%\*.*" > deploy%app%.%%a.log
  if ERRORLEVEL 0 (
    echo Deploying from %staging% to %%a ...
    robocopy \\%staging%\d\apps\%app%\ \\%%a\d\apps\%app%\ /job:deploy.rcj /LOG+:deploy%app%.%%a.log
    echo Logging deployment record to %%a ...
    elog /f:"deploycc.%%a.log" /s:"\\%%a"
  ) 
  if ERRORLEVEL 1 (
    echo !!!Cancelled deployment to %%a!!!
  )
  echo logging off %%a
  net use \\%%a /delete
)

echo logging off %staging%
net use \\%staging% /delete

endlocal
echo done

Free download of ELog

You can download ELog from CodeCentral. All Delphi source code and ELog.exe is included. I built ELog with Delphi 2010, so I recommend using Delphi 2010 or above to build or customize your own version. If you make any improvements to it, please tell me about it.


Published on: 2/9/2010 2:25:26 PM

Server Response from: ETNASC03

Copyright© 1994 - 2013 Embarcadero Technologies, Inc. All rights reserved.