Skip to content

Commit 48c42d3

Browse files
Refactor logging extensions for improved structure and functionality; update README and add default values for log levels and labels
1 parent da14801 commit 48c42d3

19 files changed

Lines changed: 240 additions & 60 deletions

.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,9 @@ dotnet_diagnostic.IDE0305.severity = none
277277
# CA1510: Use ArgumentNullException throw helper
278278
dotnet_diagnostic.CA1510.severity = none
279279

280+
# CA1031: Exception specificity
281+
dotnet_diagnostic.CA1031.severity = none
282+
280283
# IDE0306: Simplify collection initialization
281284
dotnet_diagnostic.IDE0306.severity = silent
282285

Directory.Build.props

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848

4949
<!-- Common items to include in the package -->
5050
<ItemGroup>
51-
<None Include="$(MSBuildThisFileDirectory)README.md" Pack="true" PackagePath="\" Visible="false" />
5251
<None Include="$(MSBuildThisFileDirectory)logo.png" Pack="true" PackagePath="\" Visible="false" Condition="Exists('$(MSBuildThisFileDirectory)\logo.png')" />
5352
</ItemGroup>
5453

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<Description>Provides customizable a Microsoft.Extensions.Logging.Console.ConsoleFormatter for Spectre.Console.</Description>
4+
<VersionPrefix>1.0.0</VersionPrefix>
5+
6+
<Description>Provides customizable a Microsoft.Extensions.Logging.Console.ConsoleFormatter for Spectre.Console.</Description>
57
<GenerateDocumentationFile>True</GenerateDocumentationFile>
68
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
9+
<PackageReadmeFile>README.md</PackageReadmeFile>
710
</PropertyGroup>
811

912
<ItemGroup>
10-
<PackageReference Include="Microsoft.Extensions.Logging" />
11-
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
13+
<None Include="..\README.md">
14+
<Pack>True</Pack>
15+
<PackagePath>\</PackagePath>
16+
</None>
17+
</ItemGroup>
18+
19+
<ItemGroup>
1220
<PackageReference Include="Spectre.Console" />
1321
</ItemGroup>
1422

1523
<ItemGroup>
1624
<ProjectReference Include="..\Open.Logging.Extensions\Open.Logging.Extensions.csproj" />
1725
</ItemGroup>
1826

27+
<ItemGroup>
28+
<None Update="README.md">
29+
<Pack>True</Pack>
30+
<PackagePath>\</PackagePath>
31+
</None>
32+
</ItemGroup>
33+
1934
</Project>
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# Open.Logging.Extensions.SpectreConsole
2+
3+
A lightweight integration between Microsoft's logging infrastructure and [Spectre.Console](https://spectreconsole.net/) for enhanced console logging.
4+
5+
## Overview
6+
7+
This library bridges the gap between the standard Microsoft.Extensions.Logging framework and Spectre.Console's rich styling capabilities, making it easy to use Spectre.Console as a logging target in your .NET applications.
8+
9+
## Installation
10+
11+
```sh
12+
dotnet add package Open.Logging.Extensions.SpectreConsole
13+
```
14+
15+
## Basic Usage
16+
17+
Add the Spectre Console logger to your dependency injection container:
18+
19+
```csharp
20+
using Microsoft.Extensions.Logging;
21+
using Open.Logging.Extensions.SpectreConsole;
22+
23+
// In your Startup.cs or Program.cs
24+
services.AddLogging(builder =>
25+
{
26+
builder.AddSimpleSpectreConsole();
27+
});
28+
```
29+
30+
### Inject and Use
31+
32+
Use the logger as you would any standard ILogger:
33+
34+
```csharp
35+
public class WeatherService
36+
{
37+
private readonly ILogger<WeatherService> _logger;
38+
39+
public WeatherService(ILogger<WeatherService> logger)
40+
{
41+
_logger = logger;
42+
}
43+
44+
public void GetForecastAsync()
45+
{
46+
using (_logger.BeginScope("Location: {Location}", "Seattle"))
47+
{
48+
_logger.LogInformation("Retrieving weather forecast");
49+
50+
try
51+
{
52+
// Your code here
53+
_logger.LogDebug("API request details: {Url}", "api/weather?city=Seattle");
54+
55+
// Success case
56+
_logger.LogInformation("Forecast: {Temperature}°C", 22.5);
57+
}
58+
catch (Exception ex)
59+
{
60+
_logger.LogError(ex, "Failed to retrieve forecast");
61+
}
62+
}
63+
}
64+
}
65+
```
66+
67+
## Customization
68+
69+
### Theme
70+
71+
Spectre.Console provides beautiful styling out of the box. If needed, you can customize colors and formatting:
72+
73+
```csharp
74+
var customTheme = new SpectreConsoleLogTheme
75+
{
76+
// Styles for log levels
77+
Information = new Style(foreground: Color.Cyan),
78+
Warning = new Style(foreground: Color.Yellow),
79+
Error = new Style(foreground: Color.Red),
80+
81+
// Styles for components
82+
Timestamp = new Style(foreground: Color.Grey),
83+
Category = new Style(foreground: Color.Grey, decoration: Decoration.Italic),
84+
Message = Style.Plain
85+
};
86+
87+
// Apply the custom theme
88+
services.AddLogging(builder =>
89+
{
90+
var logger = new SimpleSpectreConsoleLogger(theme: customTheme);
91+
builder.AddProvider(new SimpleSpectreConsoleLoggerProvider(logger));
92+
});
93+
```
94+
95+
### Log Level Labels
96+
97+
You can also customize the text displayed for different log levels:
98+
99+
```csharp
100+
var customLabels = new LogLevelLabels
101+
{
102+
Trace = "TRACE",
103+
Debug = "DEBUG",
104+
Information = "INFO",
105+
Warning = "WARN",
106+
Error = "ERROR",
107+
Critical = "FATAL"
108+
};
109+
110+
// Apply custom labels
111+
services.AddLogging(builder =>
112+
{
113+
var logger = new SimpleSpectreConsoleLogger(labels: customLabels);
114+
builder.AddProvider(new SimpleSpectreConsoleLoggerProvider(logger));
115+
});
116+
```
117+
118+
### Buffered Logging
119+
120+
For high-throughput applications:
121+
122+
```csharp
123+
using Open.Logging.Extensions;
124+
125+
// Get logger from DI
126+
ILogger logger = serviceProvider.GetRequiredService<ILogger<MyService>>();
127+
128+
// Create buffered logger
129+
BufferedLogger bufferedLogger = logger.AsBuffered();
130+
131+
// Use with await using for automatic flushing
132+
await using (bufferedLogger)
133+
{
134+
bufferedLogger.LogInformation("This will be buffered");
135+
}
136+
```
137+
138+
## Styling Reference
139+
140+
This integration leverages Spectre.Console's excellent styling system. You can use any style supported by Spectre.Console:
141+
142+
```csharp
143+
// Simple color names
144+
Error = "red"
145+
146+
// With decorations
147+
Warning = "bold yellow"
148+
149+
// With background
150+
Critical = "white on red"
151+
152+
// Using Style constructor
153+
Debug = new Style(Color.Blue, Color.Default, Decoration.Dim)
154+
```
155+
156+
For a complete style reference, see the [Spectre.Console documentation](https://spectreconsole.net/markup).
157+
158+
## Requirements
159+
160+
- .NET 9.0+
161+
- Microsoft.Extensions.Logging
162+
- Spectre.Console
163+
164+
## License
165+
166+
MIT License - see the LICENSE file for details.

Open.Logging.Extensions.SpectreConsole/SimpleSpectreConsoleFormatter.cs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ namespace Open.Logging.Extensions.SpectreConsole;
66
/// A formatter that outputs log entries to the console using Spectre.Console for enhanced visual styling.
77
/// </summary>
88
/// <param name="theme">The theme to use for console output styling. If null, uses <see cref="SpectreConsoleLogTheme.Default"/>.</param>
9-
/// <param name="labels">The labels to use for different log levels. If null, uses <see cref="Default.LevelLabels"/>.</param>
9+
/// <param name="labels">The labels to use for different log levels. If null, uses <see cref="Defaults.LevelLabels"/>.</param>
1010
/// <param name="writer">The console writer to use. If null, uses <see cref="AnsiConsole.Console"/>.</param>
1111
public sealed class SimpleSpectreConsoleFormatter(
1212
SpectreConsoleLogTheme? theme = null,
1313
LogLevelLabels? labels = null,
1414
IAnsiConsole? writer = null)
1515
{
1616
private readonly SpectreConsoleLogTheme _theme = theme ?? SpectreConsoleLogTheme.Default;
17-
private readonly LogLevelLabels _labels = labels ?? Default.LevelLabels;
17+
private readonly LogLevelLabels _labels = labels ?? Defaults.LevelLabels;
1818
private readonly IAnsiConsole _writer = writer ?? AnsiConsole.Console;
1919

2020
/// <summary>
@@ -31,7 +31,7 @@ public ConsoleDelegateFormatter GetConsoleFormatter(
3131
public void Write(PreparedLogEntry entry)
3232
{
3333
// Timestamp/
34-
var elapsedSeconds = entry.GetElapsed().TotalSeconds;
34+
var elapsedSeconds = entry.Elapsed.TotalSeconds;
3535
_writer.Write(new Text($"{elapsedSeconds:000.000}s", _theme.Timestamp));
3636

3737
// Level
@@ -74,21 +74,28 @@ public void Write(PreparedLogEntry entry)
7474
_writer.Write(" ");
7575
_writer.WriteStyled(entry.Message, _theme.Message);
7676
}
77+
78+
_writer.WriteLine();
79+
7780
// Add the exception details if they exist.
7881
if (entry.Exception is not null)
7982
{
80-
_writer.Write(Environment.NewLine);
83+
var rule = new Rule() { Style = Color.Grey };
84+
_writer.Write(rule);
8185
try
8286
{
8387
_writer.WriteException(entry.Exception);
8488
}
85-
catch (Exception ex)
89+
catch
8690
{
87-
// Fallback if WriteException fails
88-
_writer.Write($"Exception: {entry.Exception.Message}");
89-
_writer.Write(Environment.NewLine);
90-
_writer.Write($"Stack Trace: {entry.Exception.StackTrace}");
91+
// Fall-back if WriteException fails
92+
_writer.WriteLine($"Exception: {entry.Exception.Message}");
93+
var st = entry.Exception.StackTrace;
94+
if (!string.IsNullOrWhiteSpace(st))
95+
_writer.WriteLine(st);
9196
}
97+
98+
_writer.Write(rule);
9299
}
93100
}
94101

Open.Logging.Extensions.SpectreConsole/SimpleSpectreConsoleLogger.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ namespace Open.Logging.Extensions.SpectreConsole;
66
/// <summary>
77
/// A logger implementation that uses Spectre.Console for enhanced console output.
88
/// </summary>
9-
/// <param name="level">The minimum log level to display. Defaults to the value in <see cref="Default.LogLevel"/>.</param>
9+
/// <param name="level">The minimum log level to display. Defaults to the value in <see cref="Defaults.LogLevel"/>.</param>
1010
/// <param name="category">The optional category name for the logger.</param>
1111
/// <param name="timestamp">The optional timestamp to use for log entries. Defaults to current time.</param>
12-
/// <param name="labels">The optional custom labels for log levels. Defaults to <see cref="Default.LevelLabels"/>.</param>
12+
/// <param name="labels">The optional custom labels for log levels. Defaults to <see cref="Defaults.LevelLabels"/>.</param>
1313
/// <param name="theme">The optional custom theme for console output. Defaults to <see cref="SpectreConsoleLogTheme.Default"/>.</param>
1414
/// <param name="console">The optional <see cref="IAnsiConsole"/> instance to use for writing output. Defaults to <see cref="AnsiConsole.Console"/>.</param>
1515
/// <param name="scoped">Whether to enable log scopes. Defaults to <see langword="true"/>.</param>
1616
public class SimpleSpectreConsoleLogger(
17-
LogLevel level = Default.LogLevel,
17+
LogLevel level = Defaults.LogLevel,
1818
string? category = null,
1919
DateTimeOffset? timestamp = null,
2020
LogLevelLabels? labels = null,

Open.Logging.Extensions.SpectreConsole/SpectreConsoleExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public static class SpectreConsoleExtensions
1616
/// <param name="trim">Whether to trim whitespace from the text before writing. Default is false.</param>
1717
public static void WriteStyled(this IAnsiConsole console, string? text, Style style, bool trim = false)
1818
{
19+
ArgumentNullException.ThrowIfNull(console);
1920
if (trim ? string.IsNullOrWhiteSpace(text) : string.IsNullOrEmpty(text))
2021
return;
2122

Open.Logging.Extensions.SpectreConsole/SpectreConsoleLogTheme.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ public record SpectreConsoleLogTheme
8989
/// <param name="labels">The labels to use for the log levels.</param>
9090
/// <returns>A styled <see cref="Text"/> object for the specified log level.</returns>
9191
public Text GetTextForLevel(LogLevel logLevel, LogLevelLabels labels)
92-
=> _labelStyles.GetOrAdd(labels.GetLabelForLevel(logLevel), k => new(k, GetStyleForLevel(logLevel)));
92+
{
93+
ArgumentNullException.ThrowIfNull(labels);
94+
return _labelStyles.GetOrAdd(labels.GetLabelForLevel(logLevel), k => new(k, GetStyleForLevel(logLevel)));
95+
}
9396

9497
/// <summary>
9598
/// The default theme instance.

Open.Logging.Extensions.Tests/ConsoleDelegateFormatterTests.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public class ConsoleDelegateFormatterTests
99
public void Write_FormatsAndInvokesHandler()
1010
{
1111
// Arrange
12-
var textWriter = new StringWriter();
12+
using var textWriter = new StringWriter();
1313
PreparedLogEntry? capturedEntry = null;
1414
TextWriter? capturedWriter = null;
1515

@@ -44,7 +44,7 @@ public void Write_FormatsAndInvokesHandler()
4444
public void Write_WithScopeProvider_PassesScopesToHandler()
4545
{
4646
// Arrange
47-
var textWriter = new StringWriter();
47+
using var textWriter = new StringWriter();
4848
PreparedLogEntry? capturedEntry = null;
4949
var scopeProvider = new LoggerExternalScopeProvider();
5050

@@ -77,14 +77,14 @@ public void Write_WithScopeProvider_PassesScopesToHandler()
7777
Assert.NotNull(capturedEntry);
7878
Assert.Equal(2, capturedEntry!.Value.Scopes.Count);
7979
Assert.Equal("Scope1", capturedEntry.Value.Scopes[0].ToString());
80-
Assert.Contains("Scopes: 2", textWriter.ToString());
80+
Assert.Contains("Scopes: 2", textWriter.ToString(), StringComparison.Ordinal);
8181
}
8282

8383
[Fact]
8484
public void Write_WithException_PassesExceptionToHandler()
8585
{
8686
// Arrange
87-
var textWriter = new StringWriter();
87+
using var textWriter = new StringWriter();
8888
PreparedLogEntry? capturedEntry = null;
8989
var expectedException = new InvalidOperationException("Test exception");
9090

@@ -117,7 +117,7 @@ public void Write_WithException_PassesExceptionToHandler()
117117
public void Write_WithEmptyMessage_DoesNotInvokeHandler()
118118
{
119119
// Arrange
120-
var textWriter = new StringWriter();
120+
using var textWriter = new StringWriter();
121121
var handlerInvoked = false;
122122

123123
var formatter = new ConsoleDelegateFormatter(
@@ -148,7 +148,7 @@ public void Write_WithEmptyMessage_DoesNotInvokeHandler()
148148
public void Constructor_WithAlternateSignature_WrapsHandlerCorrectly()
149149
{
150150
// Arrange
151-
var textWriter = new StringWriter();
151+
using var textWriter = new StringWriter();
152152
PreparedLogEntry? capturedEntry = null;
153153

154154
var formatter = new ConsoleDelegateFormatter(
@@ -175,7 +175,7 @@ public void Constructor_WithAlternateSignature_WrapsHandlerCorrectly()
175175
public void Constructor_WithCustomTimestamp_UsesProvidedTimestamp()
176176
{
177177
// Arrange
178-
var textWriter = new StringWriter();
178+
using var textWriter = new StringWriter();
179179
PreparedLogEntry? capturedEntry = null;
180180
var timestamp = new DateTimeOffset(2023, 1, 1, 12, 0, 0, TimeSpan.Zero);
181181

0 commit comments

Comments
 (0)