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