Logbert is built using modern .NET technologies with a focus on cross-platform compatibility, maintainability, and extensibility. This document provides a comprehensive overview of the system architecture, design patterns, and key components.
- .NET 10.0 - Cross-platform runtime (Latest)
- C# 13 - Programming language with latest features
- Avalonia UI 11.3.8 - Cross-platform XAML-based UI framework
| Package | Version | Purpose |
|---|---|---|
| Avalonia | 11.3.8 | UI framework |
| CommunityToolkit.Mvvm | 8.4.0 | MVVM infrastructure with source generators |
| Dock.Avalonia | 11.3.6.5 | Docking panel system |
| AvaloniaEdit | 11.3.0 | Code editor control |
| MoonSharp | Latest | Lua scripting engine |
- Windows 10/11 (x64, ARM64)
- macOS 11+ (x64, ARM64)
- Linux (x64, ARM64) - GTK, X11, Wayland
Logbert strictly follows the MVVM pattern for UI components:
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐
│ View │─────▶│ ViewModel │─────▶│ Model │
│ (.axaml) │ │ (Observable │ │ (Business │
│ │◀─────│ Properties) │◀─────│ Logic) │
└─────────────┘ └──────────────────┘ └─────────────┘
│ │
│ │
▼ ▼
Data Binding INotifyPropertyChanged
Commands RelayCommand
Benefits:
- Clear separation of concerns
- Testable business logic
- Reusable ViewModels
- Designer-friendly XAML
┌──────────────────────────────────────────────────────────────┐
│ MainWindow │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ MainWindowViewModel │ │
│ │ • DockFactory (manages docking layout) │ │
│ │ • DockLayoutManager (persists layout) │ │
│ │ • Documents (collection of log tabs) │ │
│ │ • ActiveDocument (currently selected) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Filter │ │ LogViewerControl│ │ Logger Tree │ │
│ │ Panel │ │ │ │ & Bookmarks │ │
│ │ │ │ • DataGrid │ │ │ │
│ │ ViewModel │ ViewModel │ │ ViewModel │ │
│ └─────────┘ └──────────────────┘ └──────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ EnhancedLogDetailsView │ │
│ │ • Formatted message display │ │
│ │ • Exception visualization │ │
│ │ • Metadata sections │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
LogMessage (abstract)
|
┌─────────────────┼─────────────────┐
│ │ │
LogMessageLog4Net LogMessageSyslog LogMessageCustom
LogMessageNLog LogMessageEventlog LogMessageWinDebug
Base Properties:
Index- Message sequence numberTimestamp- When message was createdLevel- Log level (Trace, Debug, Info, Warning, Error, Fatal)Logger- Source logger nameMessage- Log message textRawData- Original data structure
Specialized Implementations:
LogMessageLog4Net (src/Logbert/Logging/LogMessageLog4Net.cs)
- Parses Log4Net XML format (
<log4j:event>) - Supports thread info, location data, custom properties
- Column layout: Number, Level, Timestamp, Logger, Thread, Message
LogMessageSyslog (src/Logbert/Logging/LogMessageSyslog.cs)
- Parses RFC 3164 syslog format
- Extracts Priority (Facility + Severity)
- Column layout: Number, Severity, Timestamp, Facility, Sender, Message
LogMessageCustom (src/Logbert/Logging/LogMessageCustom.cs)
- User-configurable regex-based parsing
- Dynamic column definitions via Columnizer
- Flexible for proprietary log formats
All log receivers implement ILogProvider:
public interface ILogProvider
{
string Name { get; }
string Description { get; }
string Tooltip { get; }
ILogSettingsCtrl? Settings { get; }
Dictionary<int, LogColumnData> Columns { get; }
LogLevel SupportedLevels { get; }
void Initialize(ILogHandler logHandler);
void Shutdown();
void Clear();
void Reset();
}Common functionality for all receivers:
public abstract class ReceiverBase : ILogProvider, IDisposable
{
protected ILogHandler mLogHandler;
protected Encoding mEncoding;
// Abstract properties for derived classes
public abstract string Name { get; }
public abstract string Description { get; }
public abstract ILogSettingsCtrl Settings { get; }
}File-Based Receivers:
FileSystemWatcher → File Change Event → StreamReader → Parse → LogMessage[]
↓
ILogHandler.HandleMessage()
Network Receivers:
UdpClient/TcpListener → Async Receive → Parse → LogMessage[]
↓
ILogHandler.HandleMessage()
Example: Log4NetFileReceiver (src/Logbert/Receiver/Log4NetFileReceiver/)
public class Log4NetFileReceiver : ReceiverBase
{
private FileSystemWatcher mFileWatcher;
private StreamReader mFileReader;
private long mLastFileOffset;
private void OnLogFileChanged(object sender, FileSystemEventArgs e)
{
// Read new data from file
mFileReader.BaseStream.Seek(mLastFileOffset, SeekOrigin.Begin);
// Parse XML messages
while (/* more data */)
{
string xmlData = ReadUntil("</log4j:event>");
LogMessage msg = LogMessageLog4Net.ParseData(xmlData, index);
messages.Add(msg);
}
// Send to handler
mLogHandler.HandleMessage(messages.ToArray());
}
}Location: src/Logbert/Views/MainWindow.axaml
Responsibilities:
- Application shell and menu system
- Dialog coordination
- Document lifecycle management
Key Methods:
ShowNewLogSourceDialog()- Two-step receiver selection and configurationShowAboutDialog()- About dialogShowOptionsDialog()- SettingsShowFindDialog()- SearchShowScriptEditor()- Lua script editorShowStatisticsDialog()- Log statistics
DockFactory (src/Logbert/Docking/DockFactory.cs)
- Creates and manages dock layout
- Tool panels: Filter, Logger Tree, Bookmarks
- Document area for log tabs
public class DockFactory
{
public void AddDocument(LogDocumentViewModel document)
{
var dockDocument = new Document
{
Id = $"Doc_{document.GetHashCode()}",
Title = document.Title,
Context = document
};
documentDock.VisibleDockables.Add(dockDocument);
}
}DockLayoutManager (src/Logbert/Docking/DockLayoutManager.cs)
- Persists layout to
%APPDATA%/Logbert/layout.json - Restores layout on startup
Location: src/Logbert/Views/Controls/LogViewerControl.axaml
Features:
- DataGrid with virtualization (handles 100k+ messages)
- Color-coded log levels
- ColorMap visualization on right side
- Context menu for copy, bookmark, etc.
Data Flow:
ILogProvider → LogViewerViewModel.HandleMessage()
↓
Messages.Add(logMsg)
↓
FilteredMessages (based on level filters)
↓
DataGrid ItemsSource binding
Location: src/Logbert/Views/Controls/EnhancedLogDetailsView.axaml
Sections:
- Header - Color-coded level badge
- Metadata - Timestamp, logger, thread
- Message - Main log message with wrapping
- Exception - Red background for exceptions
- Raw Data - Expandable section for full data
1. NewLogSourceDialog
↓ (user selects "Log4Net File")
2. ReceiverConfigurationDialog
↓ (hosts Log4NetFileReceiverSettingsView)
3. File picker dialog
↓ (user selects log file)
4. Validation
↓ (settings validated)
5. ILogProvider instance created
↓
6. LogDocumentViewModel created and initialized
↓
7. Document added to docking system
ReceiverConfigurationDialog (src/Logbert/Views/Dialogs/ReceiverConfigurationDialog.axaml.cs)
public ReceiverConfigurationDialog(string receiverType)
{
// Create appropriate settings control
switch (receiverType)
{
case "Log4Net File":
_settingsControl = new Log4NetFileReceiverSettingsViewModel();
break;
case "NLog File":
_settingsControl = new NLogFileReceiverSettingsViewModel();
break;
}
}Location: src/Logbert/Scripting/ScriptEngine.cs
Architecture:
Lua Script
↓
MoonSharp Interpreter
↓
C# Script Engine Wrapper
↓
LogMessage Processing
API Surface:
-- Filter function (return true to show message)
function filter(message)
return message.Level == "Error"
end
-- Process function (transform message)
function process(message)
message.Logger = string.upper(message.Logger)
return message
end
-- Access message properties
message.Index
message.Level
message.Logger
message.Message
message.Timestamp.Year
message.Timestamp.HourImplementation:
public class ScriptEngine
{
private Script _script;
public void LoadScript(string scriptText)
{
_script = new Script();
// Register types
UserData.RegisterType<LogMessage>();
UserData.RegisterType<LogLevel>();
// Add print function
_script.Globals["print"] = (Action<string>)Print;
// Execute script
_script.DoString(scriptText);
}
public bool ExecuteFilter(LogMessage message)
{
DynValue filterFunc = _script.Globals.Get("filter");
DynValue result = _script.Call(filterFunc, message);
return result.Boolean;
}
}Windows: %APPDATA%/Logbert/
macOS: ~/Library/Application Support/Logbert/
Linux: ~/.config/Logbert/
Files:
layout.json- Dock layout configurationsettings.json- Application settings (JSON-based persistence)scripts/- Saved Lua scripts (planned)
public class DockLayoutManager
{
private readonly string _layoutFilePath;
public void SaveLayout(IDock? layout)
{
var json = SerializeLayout(layout);
File.WriteAllText(_layoutFilePath, json);
}
public IDock? LoadLayout()
{
if (File.Exists(_layoutFilePath))
{
var json = File.ReadAllText(_layoutFilePath);
return DeserializeLayout(json);
}
return null;
}
}Using CommunityToolkit.Mvvm source generators:
public partial class LogViewerViewModel : ObservableObject
{
[ObservableProperty]
private double _fontSize = 11; // Generates OnPropertyChanged
[RelayCommand]
private void OnZoomIn() // Generates ZoomInCommand
{
FontSize += 2;
}
}DockFactory creates and manages dockable components:
public class DockFactory
{
public IDock CreateLayout()
{
return new ProportionalDock
{
Proportion = double.NaN,
Orientation = Orientation.Horizontal,
ActiveDockable = null,
VisibleDockables = CreateList<IDockable>
(
new ToolDock { /* tools */ },
new SplitterDock(),
new DocumentDock { /* documents */ }
)
};
}
}Different log message parsers implement different parsing strategies:
public abstract class LogMessage
{
public abstract DateTime Timestamp { get; }
public abstract string Message { get; }
public abstract LogLevel Level { get; }
}
// Concrete strategies
public class LogMessageLog4Net : LogMessage
{
// XML parsing strategy
}
public class LogMessageSyslog : LogMessage
{
// RFC 3164 parsing strategy
}ILogHandler receives updates from ILogProvider:
public interface ILogHandler
{
void HandleMessage(LogMessage logMsg);
void HandleMessage(LogMessage[] logMsgs);
void HandleError(LogError error);
}
// Receiver sends updates
mLogHandler.HandleMessage(newMessages.ToArray());- All UI operations run on Avalonia's UI thread
- Uses
Dispatcher.UIThread.Post()for cross-thread updates
public void HandleMessage(LogMessage logMsg)
{
Dispatcher.UIThread.Post(() =>
{
Messages.Add(logMsg);
FilteredMessages.Add(logMsg);
});
}File Receivers:
- FileSystemWatcher events on ThreadPool threads
- StreamReader operations on background threads
Network Receivers:
- Async UDP/TCP operations using BeginReceive/EndReceive
- Socket operations on ThreadPool threads
Avalonia DataGrid uses UI virtualization:
- Only visible rows are rendered
- Handles 100,000+ messages efficiently
- Scroll performance maintained
Receivers batch messages before sending:
private FixedSizedQueue<LogMessage> mMessages = new(100);
// Collect messages
while (hasMore)
{
mMessages.Enqueue(ParseMessage());
}
// Send batch
mLogHandler.HandleMessage(mMessages.ToArray());Uses ObservableCollection<T> with change notifications:
- Minimal UI updates
- CollectionChanged events only for actual changes
public void HandleError(LogError error)
{
Dispatcher.UIThread.Post(() =>
{
// Log to debug output
Debug.WriteLine($"Receiver error: {error.Message}");
// TODO: Show user notification
});
}Settings dialogs validate before creating receivers:
public ValidationResult ValidateSettings()
{
if (!File.Exists(FilePath))
{
return ValidationResult.Error("File does not exist");
}
return ValidationResult.Success;
}- ViewModel logic (without UI dependencies)
- Log message parsers
- Receiver data processing
- Receiver end-to-end (file monitoring, network listening)
- Script engine execution
- Data persistence
- Avalonia UI testing framework
- Automated interaction tests
ILogReceiverPlugin
↓
Plugin Discovery (MEF or custom)
↓
Dynamic Loading
↓
Configuration UI Generation
- Use Microsoft.Extensions.DependencyInjection
- Service registration in Program.cs
- Constructor injection in ViewModels
- Weak event pattern for loose coupling
- Pub/sub messaging between components