YOUSEFUL / DELPHI / SAMPLE PROJECTS
Live log viewer
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
- A
TAtlasLoggerand aTAtlasMemorySink(Capacity2000) owned by the form. - A
TAtlasLogViewwithSink,RefreshInterval := 100and a monospaced font. It polls the sink'sVersionon a main-thread timer, so events logged on other threads never touch the control directly. - Level colours: trace and debug grey, warnings amber, errors red, fatal dark red and bold.
- Log a batch: a channel, a scope nested inside a timed scope
(
[batch/checkout]), template properties, and the timer'sbatch finished in N msline. - Log a failure: an exception logged with
Failure. - Four threads: 4 x 501 events. With the capacity at 2000, older events are
pushed out of the ring buffer; the status line shows
Discarded. - The level box sets
MinLeveland the text box setsFilterText(matched against the formatted row, ignoring case). - Copy calls
CopyToClipboard(selected rows, or every shown row when nothing is selected); Clear empties the sink and the view follows. - The form waits for its worker threads before it frees the logger.
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:
LiveLogViewer.exe window.pngExpected 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):
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
- LiveLogViewer.dpr
- README.md
- shared/Atlas.TestVcl.pas snapshot helper, in the download
The ZIP contains every sample project with its README. See getting started for the search path to add.
Source
LiveLogViewer.dpr
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.