YOUSEFUL / DELPHI / SAMPLE PROJECTS
First logger
The quick start for Atlas Log and the project behind the tutorial in the
Atlas Log guide. A console program that sets
up one TAtlasLogger with five sinks, logs a short script, then reads its own
output back and checks it.
What it demonstrates
- A
TAtlasLoggerthat owns its sinks, withMinLevelon the logger (llDebug) and on individual sinks. TAtlasMemorySink: every event kept in memory and inspected throughSnapshot,TAtlasLogEvent.PropertyValueand the sequence numbers.TAtlasConsoleSinkwith a customPattern, showing warnings and above (errors go to standard error).TAtlasDebugSink: the same events for the IDE's event log or anyOutputDebugStringviewer.TAtlasFileSinktwice, in a folder under%TEMP%: a text file rotated at 512 bytes (MaxBytes,MaxFiles), and a JSON-lines file (Layout := loJson).- Message templates (
'Order {id} placed by {customer} for {total}') whose holes become properties, a channel (Log.Channel('orders')), a scope (BeginScope), a failure (Failure) and a timed scope (Time). - Reading a log file while the sink still holds it open.
Build and run
Open FirstLogger.dpr in Delphi 13; the IDE creates the project file. Add the Atlas Log src folder to the project's search path, then build and run for Win32 or Win64. No packages need to be installed. The program needs no input. It checks its own results and stops with an error if any of them differs from the output below.
The program needs no input and removes its temporary folder when it ends.
Expected output
Every check prints an ok: line; the program ends with All checks passed.
and exit code 0. Any failed check prints Exception: Check failed: ... and
sets exit code 1. Paths, the thread id, the timestamp and the elapsed
milliseconds differ on your machine:
Logging...
console: [WRN] orders Stock for AX-7 is low: 3 left
console: [ERR] orders Could not archive the order | EInOutError: Disk quota exceeded
Memory sink:
ok: 26 events reached the memory sink, the debug event included
ok: the template filled its holes and kept them as properties
ok: the failure kept the exception apart from the message
ok: the timer named the scope and reported its duration
ok: no sink failed
Text file C:\Users\you\AppData\Local\Temp\AtlasLogFirstLogger\app.log:
ok: full files were rolled into 2 archives
ok: all 25 events at info and above are in the text files, the debug event is not
ok: empty tokens leave no double spaces
ok: category, scope and properties are on the line
ok: the exception follows the message
ok: the newest line is last in the active file
[INF] [import] Imported row 17 of 20 {row=17, rows=20}
[INF] [import] Imported row 18 of 20 {row=18, rows=20}
[INF] [import] Imported row 19 of 20 {row=19, rows=20}
[INF] [import] Imported row 20 of 20 {row=20, rows=20}
[INF] [import] import finished in 2 ms {scope=import, elapsed_ms=2}
JSON file C:\Users\you\AppData\Local\Temp\AtlasLogFirstLogger\app.jsonl:
ok: one JSON object per event
{"ts":"2026-09-22T01:37:43.606Z","seq":3,"level":"info","category":"orders","thread":52800,"scope":"checkout","msg":"Order 1042 placed by Contoso for 1250.5","template":"Order {id} placed by {customer} for {total}","props":{"id":"1042","customer":"Contoso","total":"1250.5"}}
ok: the JSON object keeps message, template, scope and properties apart, with a UTC time
ok: the exception is a nested object
All checks passed.The error line goes to standard error. A console shows it in order, as above; when the output is redirected to a file it may appear separately.
Adapting it
Change Folder to your application's log folder, drop the checks, and keep
the sink setup. Raise MaxBytes to a realistic size (for example
10 * 1024 * 1024) and set MaxFiles to the number of archives to keep. To
log from a form instead, create the logger and sinks with the form as owner
and keep the TAtlasLogChannel values in fields.
Project files
The ZIP contains every sample project with its README. See getting started for the search path to add.
Source
FirstLogger.dpr
program FirstLogger;
// Atlas Log quick start. One logger writes to a memory ring buffer, the
// console (warnings and above), the debugger, a size-rotated text file and a
// JSON-lines file, both files in a temporary folder. The program logs with
// message templates, a channel, a scope, a failure and a timer, reads its own
// output back, checks it, and deletes the folder. Exit code 0 means every
// check passed.
{$APPTYPE CONSOLE}
uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Atlas.Log, Atlas.LogSinks;
procedure Check(Condition: Boolean; const What: string);
begin
if not Condition then raise Exception.Create('Check failed: ' + What);
Writeln(' ok: ', What);
end;
// The file sink keeps its file open and denies other writers, not readers.
function ReadLines(const FileName: string): TArray<string>;
var
Stream: TFileStream;
Bytes: TBytes;
begin
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyNone);
try
SetLength(Bytes, Stream.Size);
if Length(Bytes) > 0 then Stream.ReadBuffer(Bytes[0], Length(Bytes));
finally
Stream.Free;
end;
Result := TEncoding.UTF8.GetString(Bytes).Split([sLineBreak], TStringSplitOptions.ExcludeEmpty);
end;
procedure Run(const Folder: string);
var
Log: TAtlasLogger;
Memory: TAtlasMemorySink;
Console: TAtlasConsoleSink;
Debugger: TAtlasDebugSink;
Plain, Json: TAtlasFileSink;
Orders: TAtlasLogChannel;
Scope, Timer: IInterface;
Events: TArray<TAtlasLogEvent>;
Lines: TArray<string>;
Name: string;
Parsed: TJSONValue;
I, Archive, Archives: Integer;
begin
Log := TAtlasLogger.Create(nil);
try
Log.MinLevel := llDebug;
// Sinks owned by the logger are freed with it.
Memory := TAtlasMemorySink.Create(Log);
Memory.Capacity := 100;
Memory.Logger := Log;
Console := TAtlasConsoleSink.Create(Log);
Console.MinLevel := llWarning;
Console.Pattern := ' console: [{level:short}] {category} {message}{exception}';
Console.Logger := Log;
Debugger := TAtlasDebugSink.Create(Log);
Debugger.Logger := Log;
Plain := TAtlasFileSink.Create(Log);
Plain.FileName := TPath.Combine(Folder, 'app.log');
Plain.Pattern := '[{level:short}] {category} {scope} {message}{exception}{props}';
Plain.MinLevel := llInfo;
Plain.MaxBytes := 512;
Plain.MaxFiles := 5;
Plain.Logger := Log;
Json := TAtlasFileSink.Create(Log);
Json.FileName := TPath.Combine(Folder, 'app.jsonl');
Json.Layout := loJson;
Json.Logger := Log;
Writeln('Logging...');
Log.Info('Application started');
Log.Debug('Settings read from {file}', ['settings.json']);
Orders := Log.Channel('orders');
Scope := Log.BeginScope('checkout');
Orders.Info('Order {id} placed by {customer} for {total}', [1042, 'Contoso', 1250.5]);
Orders.Warning('Stock for {sku} is low: {left} left', ['AX-7', 3]);
Scope := nil;
try
raise EInOutError.Create('Disk quota exceeded');
except
on E: Exception do Orders.Failure(E, 'Could not archive the order');
end;
Timer := Log.Time('import');
for I := 1 to 20 do Log.Info('Imported row {row} of {rows}', [I, 20]);
Timer := nil;
Log.Flush;
Writeln('Memory sink:');
Events := Memory.Snapshot;
Check(Length(Events) = 26, '26 events reached the memory sink, the debug event included');
Check((Events[2].Message = 'Order 1042 placed by Contoso for 1250.5') and (Events[2].Category = 'orders') and
(Events[2].Scope = 'checkout') and (Events[2].PropertyValue('customer') = 'Contoso') and
(Events[2].Template = 'Order {id} placed by {customer} for {total}'), 'the template filled its holes and kept them as properties');
Check((Events[4].Level = llError) and (Events[4].ExceptionClass = 'EInOutError') and
(Events[4].ExceptionMessage = 'Disk quota exceeded'), 'the failure kept the exception apart from the message');
Check((Events[5].Scope = 'import') and (Events[25].Message.StartsWith('import finished in ')) and
(StrToIntDef(Events[25].PropertyValue('elapsed_ms'), -1) >= 0), 'the timer named the scope and reported its duration');
for I := 1 to High(Events) do
if Events[I].Sequence <> Events[I - 1].Sequence + 1 then Check(False, 'sequence numbers increase by one');
Check(Log.SinkErrors = 0, 'no sink failed');
Writeln('Text file ', Plain.ActiveFileName, ':');
// Oldest archive first, then the active file.
Lines := nil;
Archives := 0;
for Archive := Plain.MaxFiles downto 1 do
begin
Name := TPath.Combine(Folder, Format('app.%d.log', [Archive]));
if not TFile.Exists(Name) then Continue;
Inc(Archives);
if TFile.GetSize(Name) > Plain.MaxBytes then Check(False, Name + ' stays within MaxBytes');
Lines := Lines + ReadLines(Name);
end;
Lines := Lines + ReadLines(Plain.ActiveFileName);
Check(Archives >= 1, Format('full files were rolled into %d archives', [Archives]));
Check(Length(Lines) = 25, 'all 25 events at info and above are in the text files, the debug event is not');
Check(Lines[0] = '[INF] Application started', 'empty tokens leave no double spaces');
Check(Lines[1] = '[INF] orders [checkout] Order 1042 placed by Contoso for 1250.5 {id=1042, customer=Contoso, total=1250.5}',
'category, scope and properties are on the line');
Check(Lines[3] = '[ERR] orders Could not archive the order | EInOutError: Disk quota exceeded', 'the exception follows the message');
Check(Lines[24].StartsWith('[INF] [import] import finished in '), 'the newest line is last in the active file');
for I := 20 to 24 do Writeln(' ', Lines[I]);
Writeln('JSON file ', Json.ActiveFileName, ':');
Lines := ReadLines(Json.ActiveFileName);
Check(Length(Lines) = 26, 'one JSON object per event');
for I := 0 to High(Lines) do
begin
Parsed := TJSONObject.ParseJSONValue(Lines[I]);
if not (Parsed is TJSONObject) then Check(False, 'every line parses as a JSON object');
Parsed.Free;
end;
Writeln(' ', Lines[2]);
Parsed := TJSONObject.ParseJSONValue(Lines[2]);
try
Check((Parsed.GetValue<string>('msg') = 'Order 1042 placed by Contoso for 1250.5') and
(Parsed.GetValue<string>('category') = 'orders') and (Parsed.GetValue<string>('scope') = 'checkout') and
(Parsed.GetValue<string>('template') = 'Order {id} placed by {customer} for {total}') and
(Parsed.GetValue<string>('props.total') = '1250.5') and Parsed.GetValue<string>('ts').EndsWith('Z'),
'the JSON object keeps message, template, scope and properties apart, with a UTC time');
finally
Parsed.Free;
end;
Parsed := TJSONObject.ParseJSONValue(Lines[4]);
try
Check((Parsed.GetValue<string>('level') = 'error') and (Parsed.GetValue<string>('exception.type') = 'EInOutError'),
'the exception is a nested object');
finally
Parsed.Free;
end;
finally
// Frees the sinks too, which closes the files.
Log.Free;
end;
end;
var
Folder: string;
begin
Folder := TPath.Combine(TPath.GetTempPath, 'AtlasLogFirstLogger');
try
try
if TDirectory.Exists(Folder) then TDirectory.Delete(Folder, True);
Run(Folder);
Writeln('All checks passed.');
finally
if TDirectory.Exists(Folder) then TDirectory.Delete(Folder, True);
end;
except
on E: Exception do
begin
Writeln(E.ClassName, ': ', E.Message);
ExitCode := 1;
end;
end;
end.