Core Concepts

GPAL.Logger: Turning Events Into Files

A Logger Is Where Events Land

A logger has no Log method. You build one with a fluent chain, end it with ToGPALObject, and attach it with GPAL.WithPublishToLogger. After that, every event published anywhere in the run is offered to it, and it writes the ones it subscribed to. Nothing in the workflow has to know a logger exists. Each entry is one line carrying the timestamp, the event type, the name of the method that raised the event, and the message. Note that this is the method that raised it, not the method that wrote it, so a line points at the code you care about. WithLogEventTypes narrows what a logger takes, defaulting to everything. Loggers accumulate, so several can be attached at once and each writes only what it asked for. GPAL.RemoveLogger detaches one when a phase of work is over.

IGPALLogger runLog = GPAL.Logger

.WithRootDirectory(@"C:Logs")

.WithFilename("automation.log")

.ToGPALObject();


GPAL.WithPublishToLogger(runLog);


// From here on, anything published lands in the file

GPAL.PublishSimpleEvent(GPALEventType.NOTICE, "Login step completed");


// A second logger for errors alone, alongside the first

IGPALLogger errorLog = GPAL.Logger

.WithRootDirectory(@"C:Logs")

.WithFilename("errors.log")

.ToGPALObject();


errorLog.WithLogEventTypes(GPALEventType.ERROR | GPALEventType.EXCEPTION | GPALEventType.FAILURE);

GPAL.WithPublishToLogger(errorLog);


// A stretch of work logged to its own file, then done with

GPAL.RemoveLogger(errorLog);


// Where it actually wrote, which is not always where you thought you told it to

GPALFile written = runLog.LogFullPath;

Organizing Log Files: Directories, Filenames, and Entries

Long-running or repeated automations generate a lot of entries, so a logger gives you independent controls for organizing them. WithDirectoryStructure builds a date-based folder hierarchy under the root directory. The folders are created the first time they are needed. WithFilenamePattern appends a timestamp to the filename, so the logger rotates into a new file as time passes without any extra code. WithLoggingFormat sets the timestamp format at the start of each entry, and WithDelimiter sets the character between the fields on the line. Both can be changed at any time, on an attached logger, and the next entry uses the new setting. Because a structure and a pattern can both move the file, LogDirectory, LogFilename, and LogFullPath report where the logger actually wrote. That matters when something else needs the file, such as mailing the log at the end of a run.

// the mail object first, then the server settings and the envelope on it

IGPALMail mail = GPAL.Mail.ToGPALObject();


IGPALLogger logger = GPAL.Logger

.WithRootDirectory(@"C:Logs")

.WithDirectoryStructure(DirectoryStructure.YMD)

.WithFilenamePattern(FilenamePattern.YMDHMS)

.WithFilename("automation.log")

.WithLoggingFormat(LogDateFormat.YMD_HMS)

.ToGPALObject();


GPAL.WithPublishToLogger(logger);

GPAL.PublishSimpleEvent(GPALEventType.NOTICE, "Run started");


// Writes, for example:

// C:Logs2026821automation_20260821091500.log

// 2026-08-21 09:15:00 | NOTICE | Main | Run started


// Mail the log when the run is over, wherever it ended up

mail

.WithFromEmailAddress("robot@example.com")

.WithToEmailAddress("ops@example.com")

.WithSubject("Overnight run")

.WithAttachment(logger.LogFullPath)

.Send();

NOTE

The file extension decides nothing. A logger writes lines whatever you call the file. Directory structure, filename pattern, entry timestamp format, and delimiter are each set on their own, so use whichever combination the run needs.

Logging to a Database Instead of a File

WithDatabase redirects a logger to a table. Each entry becomes a row through the insert the GPALDatabase was configured with, and the row carries the event type in front of the message, because a database fills in its own timestamp but cannot know what kind of event it was. This is what you want when several processes or machines log to one queryable place rather than to separate local files. A write that cannot work does not take the run down. The failure lands on the database as LastError, carrying the server error number, the SQL that ran, which row was being written, and that row's values, so a workflow can look afterwards rather than wrap every publish in a try.

GPALDatabase auditDb = (GPALDatabase)GPAL.Database

.WithConnectionString("...")

.WithCreateAs("INSERT INTO AutomationLog (Entry) VALUES (@Entry)")

.WithNVarCharParameter("Entry");


IGPALLogger auditLog = GPAL.Logger.WithDatabase(auditDb).ToGPALObject();


auditLog.WithLogEventTypes(GPALEventType.USERALL);

GPAL.WithPublishToLogger(auditLog);


GPAL.PublishSimpleEvent(GPALEventType.NOTICE, "Invoice batch completed");

// Row written: NOTICE | Invoice batch completed


// Nothing threw, so ask afterwards whether it landed

if (null != auditDb.LastError)

GPAL.PublishSimpleEvent(GPALEventType.NOTICE, $"[{auditDb.LastError.Number}] on row [{auditDb.LastError.Row}]");

NOTE

A logger that cannot write says so by publishing an event, which would normally reach every logger, including the broken one. GPAL keeps events raised while a logger is writing away from loggers, so a bad path or an unreachable database reports itself once on the console and handler channels rather than looping. If a logger is attached with no file and no database, that is what it tells you.

💬 Ask GPAL