diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index afed5bb..6fabb83 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,6 +51,6 @@ jobs: $coveragePercent = [math]::Round([double]$coverage * 100, 2) Write-Host "Current line coverage: $coveragePercent%" if ($coveragePercent -lt 75) { - Write-Error "Code coverage ($coveragePercent%) is below the required threshold of 70%" + Write-Error "Code coverage ($coveragePercent%) is below the required threshold of 75%" exit 1 } \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md index 8e3baf1..a44228b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,14 +1,12 @@ -## Release Notes - Serilog.Sinks.RichTextBox.WinForms.Colored v3.0.1 +## Release Notes - Serilog.Sinks.RichTextBox.WinForms.Colored v3.1.0 -### Minor Release +### Feature Release -This minor release focuses on config improvements and performance optimization. +This feature release introduces the ability to clear and restore the RichTextBox sink output. ### What Changed -- Adjusted MaxLogLines limit From 512 to 2048 lines. -- Fixed a bug where the RichTextBox would not persist the zoom factor. -- Optimized the Concurrent Circular Buffer +- Added `Clear()` and `Restore()` operations to the circular buffer/RichTextBox sink. ### Resources diff --git a/Demo/Form1.Designer.cs b/Demo/Form1.Designer.cs index 2c40b02..049794b 100644 --- a/Demo/Form1.Designer.cs +++ b/Demo/Form1.Designer.cs @@ -47,12 +47,14 @@ private void InitializeComponent() this.btnComplex = new System.Windows.Forms.ToolStripButton(); this.toolStripSeparator8 = new System.Windows.Forms.ToolStripSeparator(); this.btnDispose = new System.Windows.Forms.ToolStripButton(); - this.toolStripSeparator5 = new System.Windows.Forms.ToolStripSeparator(); this.btnReset = new System.Windows.Forms.ToolStripButton(); this.toolStripSeparator6 = new System.Windows.Forms.ToolStripSeparator(); this.btnAutoScroll = new System.Windows.Forms.ToolStripButton(); this.panel1 = new System.Windows.Forms.Panel(); this.richTextBox1 = new System.Windows.Forms.RichTextBox(); + this.btnRestore = new System.Windows.Forms.ToolStripButton(); + this.btnClear = new System.Windows.Forms.ToolStripButton(); + this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator(); this.toolStrip1.SuspendLayout(); this.toolStrip2.SuspendLayout(); this.panel1.SuspendLayout(); @@ -166,8 +168,10 @@ private void InitializeComponent() this.btnStructure, this.btnComplex, this.toolStripSeparator8, + this.btnClear, + this.btnRestore, + this.toolStripSeparator2, this.btnDispose, - this.toolStripSeparator5, this.btnReset, this.toolStripSeparator6, this.btnAutoScroll}); @@ -242,11 +246,6 @@ private void InitializeComponent() this.btnDispose.ToolTipText = "Dispose the logger"; this.btnDispose.Click += new System.EventHandler(this.btnDispose_Click); // - // toolStripSeparator5 - // - this.toolStripSeparator5.Name = "toolStripSeparator5"; - this.toolStripSeparator5.Size = new System.Drawing.Size(6, 25); - // // btnReset // this.btnReset.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text; @@ -291,6 +290,29 @@ private void InitializeComponent() this.richTextBox1.TabIndex = 0; this.richTextBox1.Text = ""; // + // btnRestore + // + this.btnRestore.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text; + this.btnRestore.Name = "btnRestore"; + this.btnRestore.Size = new System.Drawing.Size(50, 22); + this.btnRestore.Text = "Restore"; + this.btnRestore.ToolTipText = "Restore the logger"; + this.btnRestore.Click += new System.EventHandler(this.btnRestore_Click); + // + // btnClear + // + this.btnClear.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text; + this.btnClear.Name = "btnClear"; + this.btnClear.Size = new System.Drawing.Size(38, 22); + this.btnClear.Text = "Clear"; + this.btnClear.ToolTipText = "Clear the logger"; + this.btnClear.Click += new System.EventHandler(this.btnClear_Click); + // + // toolStripSeparator2 + // + this.toolStripSeparator2.Name = "toolStripSeparator2"; + this.toolStripSeparator2.Size = new System.Drawing.Size(6, 25); + // // Form1 // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); @@ -338,9 +360,11 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripButton btnComplex; private System.Windows.Forms.ToolStripSeparator toolStripSeparator8; private System.Windows.Forms.ToolStripButton btnDispose; - private System.Windows.Forms.ToolStripSeparator toolStripSeparator5; private System.Windows.Forms.ToolStripButton btnReset; private System.Windows.Forms.ToolStripSeparator toolStripSeparator6; private System.Windows.Forms.ToolStripButton btnAutoScroll; + private System.Windows.Forms.ToolStripButton btnRestore; + private System.Windows.Forms.ToolStripButton btnClear; + private System.Windows.Forms.ToolStripSeparator toolStripSeparator2; } } diff --git a/Demo/Form1.cs b/Demo/Form1.cs index f83a72d..94c52a3 100644 --- a/Demo/Form1.cs +++ b/Demo/Form1.cs @@ -33,6 +33,7 @@ namespace Demo public partial class Form1 : Form { private RichTextBoxSinkOptions? _options; + private RichTextBoxSink? _sink; private bool _toolbarsVisible = true; public Form1() @@ -42,16 +43,37 @@ public Form1() private void Initialize() { + // This is one way to configure the sink: _options = new RichTextBoxSinkOptions( theme: ThemePresets.Literate, outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:l}{NewLine}{Exception}", formatProvider: new CultureInfo("en-US")); - var sink = new RichTextBoxSink(richTextBox1, _options); + _sink = new RichTextBoxSink(richTextBox1, _options); Log.Logger = new LoggerConfiguration() - .MinimumLevel.Verbose() - .WriteTo.Sink(sink, LogEventLevel.Verbose) - .CreateLogger(); + .MinimumLevel.Verbose() + .WriteTo.Sink(_sink, LogEventLevel.Verbose) + .CreateLogger(); + + // Intentional dead code for demonstration purposes. +#pragma warning disable CS0162 + if (false) + { + // You can also use fluent syntax to configure the sink like this: + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.RichTextBox(richTextBox1, out _sink, formatProvider: new CultureInfo("en-US")) + .CreateLogger(); + + // The out _sink is optional, but it allows you to access the sink instance. + // This is useful if you need to access the sink's methods, such as Clear() or Restore(). + // If you don't need to access the sink, you can omit the out parameter like this: + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.RichTextBox(richTextBox1, formatProvider: new CultureInfo("en-US")) + .CreateLogger(); + } +#pragma warning restore CS0162 Log.Debug("Started logger."); btnDispose.Enabled = true; @@ -337,5 +359,25 @@ private void Form1_KeyDown(object sender, KeyEventArgs e) toolStrip2.Visible = _toolbarsVisible; } } + + private void btnClear_Click(object sender, EventArgs e) + { + if (_sink == null) + { + return; + } + + _sink.Clear(); + } + + private void btnRestore_Click(object sender, EventArgs e) + { + if (_sink == null) + { + return; + } + + _sink.Restore(); + } } } \ No newline at end of file diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Collections/ConcurrentCircularBufferTests.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Collections/ConcurrentCircularBufferTests.cs new file mode 100644 index 0000000..85da98c --- /dev/null +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Collections/ConcurrentCircularBufferTests.cs @@ -0,0 +1,94 @@ +using Serilog.Sinks.RichTextBoxForms.Collections; +using Xunit; + +namespace Serilog.Tests.Collections +{ + public class ConcurrentCircularBufferTests + { + [Fact] + public void TakeSnapshot_WithItemsLessThanCapacity_ReturnsAllItemsInOrder() + { + // Arrange + var buffer = new ConcurrentCircularBuffer(3); + buffer.Add(1); + buffer.Add(2); + + // Act + var snapshot = new List(); + buffer.TakeSnapshot(snapshot); + + // Assert + Assert.Equal(new[] { 1, 2 }, snapshot); + } + + [Fact] + public void AddBeyondCapacity_OverwritesOldestItemsAndMaintainsOrder() + { + // Arrange + var buffer = new ConcurrentCircularBuffer(3); + buffer.Add(1); + buffer.Add(2); + buffer.Add(3); + buffer.Add(4); // Should overwrite the oldest item (1) + + // Act + var snapshot = new List(); + buffer.TakeSnapshot(snapshot); + + // Assert + Assert.Equal(new[] { 2, 3, 4 }, snapshot); + } + + [Fact] + public void Clear_FollowedByAdds_SnapshotContainsOnlyNewItems() + { + // Arrange + var buffer = new ConcurrentCircularBuffer(3); + buffer.Add(1); + buffer.Add(2); + buffer.Add(3); + + // Act & Assert - After clear, snapshot should be empty + buffer.Clear(); + var snapshotAfterClear = new List(); + buffer.TakeSnapshot(snapshotAfterClear); + Assert.Empty(snapshotAfterClear); + + // Add new item and verify snapshot contains only the new item + buffer.Add(4); + var snapshotAfterOneAdd = new List(); + buffer.TakeSnapshot(snapshotAfterOneAdd); + Assert.Equal(new[] { 4 }, snapshotAfterOneAdd); + + // Add two more items to fill the buffer again + buffer.Add(5); + buffer.Add(6); + var snapshotAfterMoreAdds = new List(); + buffer.TakeSnapshot(snapshotAfterMoreAdds); + Assert.Equal(new[] { 4, 5, 6 }, snapshotAfterMoreAdds); + } + + [Fact] + public void Restore_AfterClear_ReturnsAllItemsAgain() + { + // Arrange + var buffer = new ConcurrentCircularBuffer(3); + buffer.Add(1); + buffer.Add(2); + buffer.Add(3); + buffer.Clear(); + + // Add two new items while buffer is in cleared state + buffer.Add(4); + buffer.Add(5); + + // Act - Restore should make all items visible again + buffer.Restore(); + var snapshot = new List(); + buffer.TakeSnapshot(snapshot); + + // Assert + Assert.Equal(new[] { 3, 4, 5 }, snapshot); + } + } +} \ No newline at end of file diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs index d4d45e0..af977b2 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs @@ -23,6 +23,7 @@ using Serilog.Sinks.RichTextBoxForms.Rendering; using Serilog.Sinks.RichTextBoxForms.Themes; using System; +using System.Globalization; using System.Windows.Forms; namespace Serilog @@ -48,6 +49,7 @@ public static class RichTextBoxSinkLoggerConfigurationExtensions public static LoggerConfiguration RichTextBox( this LoggerSinkConfiguration sinkConfiguration, RichTextBox richTextBoxControl, + out RichTextBoxSink richTextBoxSink, Theme? theme = null, bool autoScroll = true, int maxLogLines = 256, @@ -57,10 +59,49 @@ public static LoggerConfiguration RichTextBox( LoggingLevelSwitch? levelSwitch = null) { var appliedTheme = theme ?? ThemePresets.Literate; - var renderer = new TemplateRenderer(appliedTheme, outputTemplate, formatProvider); - var options = new RichTextBoxSinkOptions(appliedTheme, autoScroll, maxLogLines, outputTemplate, formatProvider); - var sink = new RichTextBoxSink(richTextBoxControl, options, renderer); - return sinkConfiguration.Sink(sink, minimumLogEventLevel, levelSwitch); + var appliedFormatProvider = formatProvider ?? CultureInfo.InvariantCulture; + var renderer = new TemplateRenderer(appliedTheme, outputTemplate, appliedFormatProvider); + var options = new RichTextBoxSinkOptions(appliedTheme, autoScroll, maxLogLines, outputTemplate, appliedFormatProvider); + richTextBoxSink = new RichTextBoxSink(richTextBoxControl, options, renderer); + return sinkConfiguration.Sink(richTextBoxSink, minimumLogEventLevel, levelSwitch); + } + + /// + /// Adds a sink that writes log events to the specified Windows Forms + /// using colour-coded rich-text formatting. + /// + /// The logger sink configuration this extension method operates on. + /// The target instance that will display the log output. + /// Optional theme controlling colours of individual message tokens. When null, is used. + /// When true (default) the control automatically scrolls to the newest log entry. + /// Maximum number of log events retained in the circular buffer and rendered in the control. + /// Message template that controls the textual representation of each log event. + /// Culture-specific or custom formatting provider, or null to use the invariant culture. + /// Minimum level below which events are ignored by this sink. + /// Optional switch allowing the minimum log level to be changed at runtime. + /// A object that can be further configured. + public static LoggerConfiguration RichTextBox( + this LoggerSinkConfiguration sinkConfiguration, + RichTextBox richTextBoxControl, + Theme? theme = null, + bool autoScroll = true, + int maxLogLines = 256, + string outputTemplate = OutputTemplate, + IFormatProvider? formatProvider = null, + LogEventLevel minimumLogEventLevel = LogEventLevel.Verbose, + LoggingLevelSwitch? levelSwitch = null) + { + return RichTextBox( + sinkConfiguration, + richTextBoxControl, + out _, + theme, + autoScroll, + maxLogLines, + outputTemplate, + formatProvider, + minimumLogEventLevel, + levelSwitch); } } } \ No newline at end of file diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Serilog.Sinks.RichTextBox.WinForms.Colored.csproj b/Serilog.Sinks.RichTextBox.WinForms.Colored/Serilog.Sinks.RichTextBox.WinForms.Colored.csproj index 4ed5008..9e77cf9 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Serilog.Sinks.RichTextBox.WinForms.Colored.csproj +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Serilog.Sinks.RichTextBox.WinForms.Colored.csproj @@ -22,11 +22,9 @@ net462;net471;net6.0-windows;net8.0-windows;net9.0-windows;netcoreapp3.0-windows;netcoreapp3.1-windows true true - 3.0.1 + 3.1.0 - - Adjusted MaxLogLines constraint from 512 to 2048 lines. - - Fixed bug where the RTB control would not persist the zoom factor. - - Minor improvements to the concurrent circular buffer. + - Added ability to clear and restore the RichTextBox sink output. See repository for more information: https://github.com/vonhoff/Serilog.Sinks.RichTextBox.WinForms.Colored diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Collections/ConcurrentCircularBuffer.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Collections/ConcurrentCircularBuffer.cs index 3dc9924..dc28afd 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Collections/ConcurrentCircularBuffer.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/Collections/ConcurrentCircularBuffer.cs @@ -2,18 +2,20 @@ namespace Serilog.Sinks.RichTextBoxForms.Collections { - internal sealed class ConcurrentCircularBuffer + public sealed class ConcurrentCircularBuffer { private readonly object _sync = new(); private readonly T[] _buffer; private readonly int _capacity; private int _head; private int _count; + private int _itemsToSkip; public ConcurrentCircularBuffer(int capacity) { _capacity = capacity > 0 ? capacity : 1; _buffer = new T[_capacity]; + _itemsToSkip = 0; } public void Add(T item) @@ -33,6 +35,11 @@ public void Add(T item) { _head = 0; } + + if (_itemsToSkip > 0) + { + _itemsToSkip--; + } } else { @@ -47,9 +54,15 @@ public void TakeSnapshot(List target) { target.Clear(); - for (var i = 0; i < _count; ++i) + var itemsToTake = _count - _itemsToSkip; + if (itemsToTake <= 0) { - var index = _head + i; + return; + } + + for (var i = 0; i < itemsToTake; ++i) + { + var index = _head + _itemsToSkip + i; if (index >= _capacity) { index -= _capacity; @@ -59,5 +72,21 @@ public void TakeSnapshot(List target) } } } + + public void Clear() + { + lock (_sync) + { + _itemsToSkip = _count; + } + } + + public void Restore() + { + lock (_sync) + { + _itemsToSkip = 0; + } + } } } diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSink.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSink.cs index 09fad68..e963e16 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSink.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSink.cs @@ -78,6 +78,23 @@ public void Dispose() public void Emit(LogEvent logEvent) { _buffer.Add(logEvent); + Update(); + } + + public void Clear() + { + _buffer.Clear(); + Update(); + } + + public void Restore() + { + _buffer.Restore(); + Update(); + } + + private void Update() + { Interlocked.Exchange(ref _hasNewMessages, 1); _signal.Set(); } @@ -107,8 +124,6 @@ private void ProcessMessages(CancellationToken token) } Interlocked.Exchange(ref _hasNewMessages, 0); - - // Take a snapshot of the current buffer _buffer.TakeSnapshot(snapshot); builder.Clear(); foreach (var evt in snapshot) diff --git a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs index 10da5d7..78adf60 100644 --- a/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs +++ b/Serilog.Sinks.RichTextBox.WinForms.Colored/Sinks/RichTextBoxForms/RichTextBoxSinkOptions.cs @@ -18,6 +18,7 @@ using Serilog.Sinks.RichTextBoxForms.Themes; using System; +using System.Globalization; namespace Serilog.Sinks.RichTextBoxForms { @@ -46,7 +47,7 @@ public RichTextBoxSinkOptions( Theme = theme; MaxLogLines = maxLogLines; OutputTemplate = outputTemplate; - FormatProvider = formatProvider; + FormatProvider = formatProvider ?? CultureInfo.InvariantCulture; } public bool AutoScroll { get; set; }