YOUSEFUL / DELPHI / SAMPLE PROJECTS

Live log viewer

VCL application Product: Atlas Log · Components: TAtlasLogger, TAtlasMemorySink, TAtlasLogView

A VCL program with a TAtlasLogView showing a TAtlasMemorySink while the application logs, from the main thread and from worker threads. The form is built in code; there is no .dfm.

What it demonstrates

Build and run

Open LiveLogViewer.dpr in Delphi 13; the IDE creates the project file. Add the Atlas Log src folder and the shared folder to the project's search path, then build and run for Win32 or Win64. No packages need to be installed. The shared folder is part of the sample download. It holds Atlas.TestVcl.pas, which the optional snapshot mode uses to save a picture of the window.

Run it without arguments to use the window. Run it with a file name to log one batch, save a PNG of the window and exit:

Example
LiveLogViewer.exe window.png

Expected result

A 1000 x 560 window with a button bar, a level box, a filter box, the log view and a status line. After Log a batch the view shows seven rows like these (times differ):

Example
21:37:53.127 INF [batch] Batch started
21:37:53.127 TRC [batch] Reading 3 rows {count=3}
21:37:53.127 INF orders [batch/checkout] Order 1042 placed by Contoso for 1250.5 {id=1042, customer=Contoso, total=1250.5}
21:37:53.127 WRN orders [batch/checkout] Stock for AX-7 is low: 3 left {sku=AX-7, left=3}
21:37:53.127 ERR orders [batch/checkout] Payment 77 was declined {id=77}
21:37:53.127 FTL [batch] Batch aborted
21:37:53.127 DBG [batch] batch finished in 0 ms {scope=batch, elapsed_ms=0}

The status line reads 7 of 7 events shown, 0 pushed out of the ring buffer. The view keeps the newest row in sight unless you select a row further up.

Adapting it

Put the logger and memory sink wherever your application creates its logging (a data module, or the shared AtlasLog), and drop a TAtlasLogView on a diagnostics form with Sink pointing at the memory sink. Change the row format with Pattern, for example '{time:hh:nn:ss} {level:short} {category} {message}'. Give the memory sink a capacity that matches how far back people need to look; the view's cost does not grow with it, because only visible rows are drawn.

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

LiveLogViewer.dpr

pascal
program LiveLogViewer;

// Atlas Log viewer: a TAtlasLogView shows a TAtlasMemorySink live while the
// main thread and worker threads log. A level box and a text box filter the
// rows, Copy puts rows on the clipboard, Clear empties the sink. The form is
// built in code; no .dfm is needed.
//
// Run with a file name argument to save a PNG of the window instead of
// showing it:  LiveLogViewer.exe window.png

uses
  System.SysUtils, System.Classes, System.Generics.Collections, Vcl.Forms, Vcl.Controls,
  Vcl.StdCtrls, Vcl.ExtCtrls, Atlas.Log, Atlas.LogSinks, Atlas.LogView, Atlas.TestVcl;

type
  TViewerForm = class(TForm)
  private
    FLogger: TAtlasLogger;
    FMemory: TAtlasMemorySink;
    FView: TAtlasLogView;
    FLevel: TComboBox;
    FFilter: TEdit;
    FStatus: TLabel;
    FStatusTimer: TTimer;
    FWorkers: TObjectList<TThread>;
    procedure AddButton(Parent: TWinControl; var Left: Integer; const Caption: string; Handler: TNotifyEvent);
    procedure LogBatchClicked(Sender: TObject);
    procedure FailureClicked(Sender: TObject);
    procedure ThreadsClicked(Sender: TObject);
    procedure CopyClicked(Sender: TObject);
    procedure ClearClicked(Sender: TObject);
    procedure LevelChanged(Sender: TObject);
    procedure FilterChanged(Sender: TObject);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure LogBatch;
    procedure UpdateStatus(Sender: TObject);
    property View: TAtlasLogView read FView;
  end;

constructor TViewerForm.Create(AOwner: TComponent);
var Bar: TPanel; X: Integer;
begin
  inherited CreateNew(AOwner);
  Caption := 'Atlas Log: live viewer';
  Width := 1000;
  Height := 560;
  Position := poScreenCenter;
  FWorkers := TObjectList<TThread>.Create;

  FLogger := TAtlasLogger.Create(Self);
  FLogger.MinLevel := llTrace;
  FMemory := TAtlasMemorySink.Create(Self);
  FMemory.Capacity := 2000;
  FMemory.Logger := FLogger;

  Bar := TPanel.Create(Self);
  Bar.Parent := Self;
  Bar.Align := alTop;
  Bar.Height := 40;
  Bar.BevelOuter := bvNone;
  X := 8;
  AddButton(Bar, X, 'Log a batch', LogBatchClicked);
  AddButton(Bar, X, 'Log a failure', FailureClicked);
  AddButton(Bar, X, 'Four threads', ThreadsClicked);
  AddButton(Bar, X, 'Copy', CopyClicked);
  AddButton(Bar, X, 'Clear', ClearClicked);
  FLevel := TComboBox.Create(Self);
  FLevel.Parent := Bar;
  FLevel.SetBounds(X, 8, 100, 24);
  FLevel.Style := csDropDownList;
  FLevel.Items.CommaText := 'Trace,Debug,Info,Warning,Error,Fatal';
  FLevel.ItemIndex := 0;
  FLevel.OnChange := LevelChanged;
  Inc(X, 110);
  FFilter := TEdit.Create(Self);
  FFilter.Parent := Bar;
  FFilter.SetBounds(X, 8, 200, 24);
  FFilter.TextHint := 'Filter';
  FFilter.OnChange := FilterChanged;

  FStatus := TLabel.Create(Self);
  FStatus.Parent := Self;
  FStatus.Align := alBottom;
  FStatus.AlignWithMargins := True;

  FView := TAtlasLogView.Create(Self);
  FView.Parent := Self;
  FView.Align := alClient;
  FView.Font.Name := 'Consolas';
  FView.RefreshInterval := 100;
  FView.Sink := FMemory;

  FStatusTimer := TTimer.Create(Self);
  FStatusTimer.Interval := 250;
  FStatusTimer.OnTimer := UpdateStatus;
  UpdateStatus(nil);
end;

destructor TViewerForm.Destroy;
var Worker: TThread;
begin
  // Workers log through FLogger, so they must finish before the form frees it.
  for Worker in FWorkers do Worker.WaitFor;
  FWorkers.Free;
  inherited;
end;

procedure TViewerForm.AddButton(Parent: TWinControl; var Left: Integer; const Caption: string; Handler: TNotifyEvent);
var Button: TButton;
begin
  Button := TButton.Create(Self);
  Button.Parent := Parent;
  Button.SetBounds(Left, 7, 100, 26);
  Button.Caption := Caption;
  Button.OnClick := Handler;
  Inc(Left, 106);
end;

procedure TViewerForm.LogBatch;
var Orders: TAtlasLogChannel; Scope, Timer: IInterface;
begin
  Timer := FLogger.Time('batch', llDebug);
  Orders := FLogger.Channel('orders');
  FLogger.Info('Batch started');
  FLogger.Trace('Reading {count} rows', [3]);
  Scope := FLogger.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]);
  Orders.Error('Payment {id} was declined', [77]);
  Scope := nil;
  FLogger.Fatal('Batch aborted');
end;

procedure TViewerForm.LogBatchClicked(Sender: TObject);
begin
  LogBatch;
end;

procedure TViewerForm.FailureClicked(Sender: TObject);
begin
  try
    StrToInt('not a number');
  except
    on E: Exception do FLogger.Failure(E, 'Parsing user input failed');
  end;
end;

procedure TViewerForm.ThreadsClicked(Sender: TObject);
var I: Integer; Logger: TAtlasLogger; Worker: TThread;
begin
  Logger := FLogger;
  for I := 1 to 4 do
  begin
    Worker := TThread.CreateAnonymousThread(
      procedure
      var Step: Integer; Log: TAtlasLogChannel;
      begin
        Log := Logger.Channel('worker');
        for Step := 1 to 500 do Log.Trace('Step {step} of 500', [Step]);
        Log.Info('Worker finished');
      end);
    // Kept, not self-freeing, so the form can wait for them when it closes.
    Worker.FreeOnTerminate := False;
    FWorkers.Add(Worker);
    Worker.Start;
  end;
end;

procedure TViewerForm.CopyClicked(Sender: TObject);
begin
  FView.CopyToClipboard;
end;

procedure TViewerForm.ClearClicked(Sender: TObject);
begin
  // The view notices the sink's Version changing on its next timer tick.
  FMemory.Clear;
end;

procedure TViewerForm.LevelChanged(Sender: TObject);
begin
  FView.MinLevel := TAtlasLogLevel(FLevel.ItemIndex);
end;

procedure TViewerForm.FilterChanged(Sender: TObject);
begin
  FView.FilterText := FFilter.Text;
end;

procedure TViewerForm.UpdateStatus(Sender: TObject);
begin
  FStatus.Caption := Format('%d of %d events shown, %d pushed out of the ring buffer',
    [FView.ShownCount, FMemory.Count, FMemory.Discarded]);
end;

var
  Form: TViewerForm;
begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  if ParamCount >= 1 then
  begin
    Form := TViewerForm.Create(nil);
    try
      Form.LogBatch;
      Form.View.Reload;
      Form.UpdateStatus(nil);
      SaveFormImage(Form, ParamStr(1));
    finally
      Form.Free;
    end;
  end
  else
  begin
    Application.CreateForm(TViewerForm, Form);
    Application.Run;
  end;
end.