YOUSEFUL / DELPHI / SAMPLE PROJECTS

First logger

Console program Self-checking Product: Atlas Log · Components: TAtlasLogger, TAtlasMemorySink, TAtlasFileSink, TAtlasConsoleSink, TAtlasDebugSink

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

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:

Example
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

Download all 34 projects

The ZIP contains every sample project with its README. See getting started for the search path to add.

Source

FirstLogger.dpr

pascal
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.