Skip to content

Commit db442d2

Browse files
committed
Add pretty-print JSON support to RichTextBox sink
Introduces pretty-printing for JSON output in the RichTextBox sink, controlled via new options.
1 parent 4a770d9 commit db442d2

File tree

17 files changed

+577
-116
lines changed

17 files changed

+577
-116
lines changed

CODE_OF_CONDUCT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ a project may be further defined and clarified by project maintainers.
5555
## Enforcement
5656

5757
Instances of abusive, harassing, or otherwise unacceptable behavior may be
58-
reported by contacting the project team at `simon.vonhoff[at]outlook.com`. All
58+
reported by contacting the project team at `simon.vonhoff@outlook.com`. All
5959
complaints will be reviewed and investigated and will result in a response that
6060
is deemed necessary and appropriate to the circumstances. The project team is
6161
obligated to maintain confidentiality with regard to the reporter of an incident.

Demo/Form1.Designer.cs

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Demo/Form1.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public partial class Form1 : Form
3535
private RichTextBoxSinkOptions? _options;
3636
private RichTextBoxSink? _sink;
3737
private bool _toolbarsVisible = true;
38+
private bool _prettyPrintJson = false;
3839

3940
public Form1()
4041
{
@@ -47,7 +48,8 @@ private void Initialize()
4748
_options = new RichTextBoxSinkOptions(
4849
theme: ThemePresets.Literate,
4950
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:l}{NewLine}{Exception}",
50-
formatProvider: new CultureInfo("en-US"));
51+
formatProvider: new CultureInfo("en-US"),
52+
prettyPrintJson: _prettyPrintJson);
5153

5254
_sink = new RichTextBoxSink(richTextBox1, _options);
5355
Log.Logger = new LoggerConfiguration()
@@ -77,6 +79,7 @@ private void Initialize()
7779

7880
Log.Debug("Started logger.");
7981
btnDispose.Enabled = true;
82+
btnPrettyPrint.Text = _prettyPrintJson ? "Disable Pretty Print" : "Enable Pretty Print";
8083
}
8184

8285
private void Form1_Load(object sender, EventArgs e)
@@ -350,6 +353,18 @@ private void btnAutoScroll_Click(object sender, EventArgs e)
350353
btnAutoScroll.Text = _options.AutoScroll ? "Disable Auto Scroll" : "Enable Auto Scroll";
351354
}
352355

356+
private void btnPrettyPrint_Click(object sender, EventArgs e)
357+
{
358+
_prettyPrintJson = !_prettyPrintJson;
359+
btnPrettyPrint.Text = _prettyPrintJson ? "Disable Pretty Print" : "Enable Pretty Print";
360+
361+
// Recreate the sink and logger with new pretty print setting
362+
CloseAndFlush();
363+
Initialize();
364+
365+
Log.Information("Pretty print JSON: {PrettyPrint}", _prettyPrintJson);
366+
}
367+
353368
private void Form1_KeyDown(object sender, KeyEventArgs e)
354369
{
355370
if (e.Control && e.KeyCode == Keys.T)

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,16 @@ A [Serilog](https://github.com/serilog/serilog) sink that writes log events to a
1515
- Multiple theme presets with customization options
1616
- High-performance asynchronous processing
1717
- Line limit to control memory usage
18+
- Pretty printing of JSON objects
1819
- WCAG compliant color schemes based on the [Serilog WPF RichTextBox](https://github.com/serilog-contrib/serilog-sinks-richtextbox) sink.
1920

20-
## Get Started
21+
> **Consider supporting the development of this project on Ko-fi!**
22+
>
23+
> Your support keeps this project alive. Even a small gesture means a lot.
24+
>
25+
> [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/vonhoff)
26+
27+
## Getting Started
2128

2229
Install the package from NuGet:
2330

SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Reporting a Vulnerability
44

5-
If you find a security issue, please email `simon.vonhoff[at]outlook.com` with a description. Include a suggested fix if you have one.
5+
If you find a security issue, please email `simon.vonhoff@outlook.com` with a description. Include a suggested fix if you have one.
66

77
We will review the report, release a fix or mitigation if needed, and credit you. Do **not** disclose the issue publicly until a fix is released. Once addressed, public disclosure is allowed.
88

Serilog.Sinks.RichTextBox.WinForms.Colored.Test/Integration/JsonFormattingTests.cs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
using System;
12
using Serilog.Events;
3+
using Serilog.Sinks.RichTextBoxForms;
4+
using Serilog.Sinks.RichTextBoxForms.Themes;
25
using Xunit;
36

47
namespace Serilog.Tests.Integration
@@ -92,5 +95,163 @@ public void ControlCharacters_AreEscapedAsUnicode()
9295
Assert.Equal(expected, RenderAndGetText(strEvent, "{Message:j}"));
9396
}
9497
}
98+
99+
[Fact]
100+
public void PrettyPrintJson_FormatsNestedObjectsWithIndentation()
101+
{
102+
var nestedProp = new LogEventProperty("Nested", new StructureValue(new[]
103+
{
104+
new LogEventProperty("Id", new ScalarValue(123)),
105+
new LogEventProperty("Name", new ScalarValue("test"))
106+
}, "MyObj"));
107+
var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Nested:j}"), new[] { nestedProp });
108+
109+
var options = new RichTextBoxSinkOptions(
110+
theme: _defaultTheme,
111+
prettyPrintJson: true,
112+
indentSize: 4,
113+
useSpacesForIndent: true);
114+
115+
var result = RenderAndGetText(logEvent, "{Message:l}", options);
116+
var expected = "{\n \"Id\": 123,\n \"Name\": \"test\",\n \"$type\": \"MyObj\"\n}";
117+
Assert.Equal(expected, result);
118+
}
119+
120+
[Fact]
121+
public void PrettyPrintJson_FormatsArraysWithIndentation()
122+
{
123+
var arrayProp = new LogEventProperty("Array", new SequenceValue(new[]
124+
{
125+
new ScalarValue(1),
126+
new ScalarValue(2),
127+
new ScalarValue(3)
128+
}));
129+
var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Array:j}"), new[] { arrayProp });
130+
131+
var options = new RichTextBoxSinkOptions(
132+
theme: _defaultTheme,
133+
prettyPrintJson: true,
134+
indentSize: 4,
135+
useSpacesForIndent: true);
136+
137+
var result = RenderAndGetText(logEvent, "{Message:l}", options);
138+
var expected = "[\n 1,\n 2,\n 3\n]";
139+
Assert.Equal(expected, result);
140+
}
141+
142+
[Fact]
143+
public void PrettyPrintJson_FormatsDictionariesWithIndentation()
144+
{
145+
var dict = new Dictionary<ScalarValue, LogEventPropertyValue>
146+
{
147+
{ new ScalarValue("a"), new ScalarValue(1) },
148+
{ new ScalarValue("b"), new ScalarValue("hello") }
149+
};
150+
var dictProp = new LogEventProperty("DictProp", new DictionaryValue(dict));
151+
var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{DictProp:j}"), new[] { dictProp });
152+
153+
var options = new RichTextBoxSinkOptions(
154+
theme: _defaultTheme,
155+
prettyPrintJson: true,
156+
indentSize: 4,
157+
useSpacesForIndent: true);
158+
159+
var result = RenderAndGetText(logEvent, "{Message:l}", options);
160+
var expected = "{\n \"a\": 1,\n \"b\": \"hello\"\n}";
161+
Assert.Equal(expected, result);
162+
}
163+
164+
[Fact]
165+
public void PrettyPrintJson_FormatsNestedStructures()
166+
{
167+
var inner = new StructureValue(new[]
168+
{
169+
new LogEventProperty("Value", new ScalarValue(42))
170+
}, "Inner");
171+
var outer = new LogEventProperty("Outer", new StructureValue(new[]
172+
{
173+
new LogEventProperty("Inner", inner),
174+
new LogEventProperty("Name", new ScalarValue("test"))
175+
}, "Outer"));
176+
var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Outer:j}"), new[] { outer });
177+
178+
var options = new RichTextBoxSinkOptions(
179+
theme: _defaultTheme,
180+
prettyPrintJson: true,
181+
indentSize: 4,
182+
useSpacesForIndent: true);
183+
184+
var result = RenderAndGetText(logEvent, "{Message:l}", options);
185+
var expected = "{\n \"Inner\": {\n \"Value\": 42,\n \"$type\": \"Inner\"\n },\n \"Name\": \"test\",\n \"$type\": \"Outer\"\n}";
186+
Assert.Equal(expected, result);
187+
}
188+
189+
[Fact]
190+
public void PrettyPrintJson_EmptyCollectionsFormatCorrectly()
191+
{
192+
var emptyArray = new LogEventProperty("EmptyArray", new SequenceValue(Array.Empty<LogEventPropertyValue>()));
193+
var emptyObject = new LogEventProperty("EmptyObject", new StructureValue(Array.Empty<LogEventProperty>(), null));
194+
var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("Array: {EmptyArray:j}, Object: {EmptyObject:j}"), new[] { emptyArray, emptyObject });
195+
196+
var options = new RichTextBoxSinkOptions(
197+
theme: _defaultTheme,
198+
prettyPrintJson: true,
199+
indentSize: 4,
200+
useSpacesForIndent: true);
201+
202+
var result = RenderAndGetText(logEvent, "{Message:l}", options);
203+
Assert.Contains("Array: []", result);
204+
Assert.Contains("Object: {}", result);
205+
}
206+
207+
[Fact]
208+
public void PrettyPrintJson_UsesTabsWhenConfigured()
209+
{
210+
var prop = new LogEventProperty("Test", new StructureValue(new[]
211+
{
212+
new LogEventProperty("Id", new ScalarValue(123))
213+
}, "MyObj"));
214+
var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Test:j}"), new[] { prop });
215+
216+
var options = new RichTextBoxSinkOptions(
217+
theme: _defaultTheme,
218+
prettyPrintJson: true,
219+
indentSize: 4,
220+
useSpacesForIndent: false);
221+
222+
var result = RenderAndGetText(logEvent, "{Message:l}", options);
223+
var expected = "{\n\t\"Id\": 123,\n\t\"$type\": \"MyObj\"\n}";
224+
Assert.Equal(expected, result);
225+
}
226+
227+
[Fact]
228+
public void PrettyPrintJson_RespectsIndentSize()
229+
{
230+
var prop = new LogEventProperty("Test", new StructureValue(new[]
231+
{
232+
new LogEventProperty("Id", new ScalarValue(123))
233+
}, "MyObj"));
234+
var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("{Test:j}"), new[] { prop });
235+
236+
var options = new RichTextBoxSinkOptions(
237+
theme: _defaultTheme,
238+
prettyPrintJson: true,
239+
indentSize: 2,
240+
useSpacesForIndent: true);
241+
242+
var result = RenderAndGetText(logEvent, "{Message:l}", options);
243+
var expected = "{\n \"Id\": 123,\n \"$type\": \"MyObj\"\n}";
244+
Assert.Equal(expected, result);
245+
}
246+
247+
[Fact]
248+
public void CompactJson_StillWorksByDefault()
249+
{
250+
var complexProp = new LogEventProperty("ComplexProp", new StructureValue(new[] { new LogEventProperty("Id", new ScalarValue(123)) }, "MyObj"));
251+
var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, _parser.Parse("Value: {ComplexProp}"), new[] { complexProp });
252+
253+
// Default behavior (compact)
254+
Assert.Equal("Value: {\"Id\": 123, \"$type\": \"MyObj\"}", RenderAndGetText(logEvent, "{Message:j}"));
255+
}
95256
}
96257
}

Serilog.Sinks.RichTextBox.WinForms.Colored.Test/RichTextBoxSinkTestBase.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ protected string RenderAndGetText(LogEvent logEvent, string outputTemplate, IFor
4545
return _richTextBox.Text.TrimEnd('\n', '\r');
4646
}
4747

48+
protected string RenderAndGetText(LogEvent logEvent, string outputTemplate, RichTextBoxSinkOptions options)
49+
{
50+
_richTextBox.Clear();
51+
var renderer = new TemplateRenderer(options.Theme, outputTemplate, options.FormatProvider, options);
52+
renderer.Render(logEvent, _canvas);
53+
return _richTextBox.Text.TrimEnd('\n', '\r');
54+
}
55+
4856
public virtual void Dispose()
4957
{
5058
GC.SuppressFinalize(this);

Serilog.Sinks.RichTextBox.WinForms.Colored/RichTextBoxSinkLoggerConfigurationExtensions.cs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ public static class RichTextBoxSinkLoggerConfigurationExtensions
4545
/// <param name="formatProvider">Format provider, or <c>null</c> for invariant culture.</param>
4646
/// <param name="minimumLogEventLevel">Minimum log level for events to be written.</param>
4747
/// <param name="levelSwitch">Optional switch to change the minimum log level at runtime.</param>
48+
/// <param name="prettyPrintJson">If <c>true</c>, formats JSON values with indentation and line breaks. Defaults to <c>false</c>.</param>
49+
/// <param name="indentSize">Number of spaces per indentation level when pretty printing JSON. Defaults to 4.</param>
50+
/// <param name="useSpacesForIndent">If <c>true</c> (default), uses spaces for indentation; otherwise uses tabs.</param>
4851
/// <returns>The logger configuration, for chaining.</returns>
4952
public static LoggerConfiguration RichTextBox(
5053
this LoggerSinkConfiguration sinkConfiguration,
@@ -56,12 +59,15 @@ public static LoggerConfiguration RichTextBox(
5659
string outputTemplate = OutputTemplate,
5760
IFormatProvider? formatProvider = null,
5861
LogEventLevel minimumLogEventLevel = LogEventLevel.Verbose,
59-
LoggingLevelSwitch? levelSwitch = null)
62+
LoggingLevelSwitch? levelSwitch = null,
63+
bool prettyPrintJson = false,
64+
int indentSize = 4,
65+
bool useSpacesForIndent = true)
6066
{
6167
var appliedTheme = theme ?? ThemePresets.Literate;
6268
var appliedFormatProvider = formatProvider ?? CultureInfo.InvariantCulture;
63-
var renderer = new TemplateRenderer(appliedTheme, outputTemplate, appliedFormatProvider);
64-
var options = new RichTextBoxSinkOptions(appliedTheme, autoScroll, maxLogLines, outputTemplate, appliedFormatProvider);
69+
var options = new RichTextBoxSinkOptions(appliedTheme, autoScroll, maxLogLines, outputTemplate, appliedFormatProvider, prettyPrintJson, indentSize, useSpacesForIndent);
70+
var renderer = new TemplateRenderer(appliedTheme, outputTemplate, appliedFormatProvider, options);
6571
richTextBoxSink = new RichTextBoxSink(richTextBoxControl, options, renderer);
6672
return sinkConfiguration.Sink(richTextBoxSink, minimumLogEventLevel, levelSwitch);
6773
}
@@ -78,6 +84,9 @@ public static LoggerConfiguration RichTextBox(
7884
/// <param name="formatProvider">Format provider, or <c>null</c> for invariant culture.</param>
7985
/// <param name="minimumLogEventLevel">Minimum log level for events to be written.</param>
8086
/// <param name="levelSwitch">Optional switch to change the minimum log level at runtime.</param>
87+
/// <param name="prettyPrintJson">If <c>true</c>, formats JSON values with indentation and line breaks. Defaults to <c>false</c>.</param>
88+
/// <param name="indentSize">Number of spaces per indentation level when pretty printing JSON. Defaults to 4.</param>
89+
/// <param name="useSpacesForIndent">If <c>true</c> (default), uses spaces for indentation; otherwise uses tabs.</param>
8190
/// <returns>The logger configuration, for chaining.</returns>
8291
public static LoggerConfiguration RichTextBox(
8392
this LoggerSinkConfiguration sinkConfiguration,
@@ -88,7 +97,10 @@ public static LoggerConfiguration RichTextBox(
8897
string outputTemplate = OutputTemplate,
8998
IFormatProvider? formatProvider = null,
9099
LogEventLevel minimumLogEventLevel = LogEventLevel.Verbose,
91-
LoggingLevelSwitch? levelSwitch = null)
100+
LoggingLevelSwitch? levelSwitch = null,
101+
bool prettyPrintJson = false,
102+
int indentSize = 4,
103+
bool useSpacesForIndent = true)
92104
{
93105
return RichTextBox(
94106
sinkConfiguration,
@@ -100,7 +112,10 @@ public static LoggerConfiguration RichTextBox(
100112
outputTemplate,
101113
formatProvider,
102114
minimumLogEventLevel,
103-
levelSwitch);
115+
levelSwitch,
116+
prettyPrintJson,
117+
indentSize,
118+
useSpacesForIndent);
104119
}
105120
}
106121
}

0 commit comments

Comments
 (0)