Skip to content

Commit baa7684

Browse files
committed
feat(logging): full async logger with per-level Debug/Release filters, caller context, file+console output, retention #65 #62 #64 #66 #67
1 parent 009b174 commit baa7684

13 files changed

Lines changed: 315 additions & 3 deletions

File tree

src/Trion.API/Trion.API.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@
1010
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
1111
</ItemGroup>
1212

13+
<ItemGroup>
14+
<ProjectReference Include="..\Trion.Core\Trion.Core.csproj" />
15+
</ItemGroup>
16+
1317
</Project>

src/Trion.Core/Logging/LogEntry.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace Trion.Core.Logging;
2+
3+
internal readonly record struct LogEntry(
4+
DateTimeOffset Timestamp,
5+
LogLevel Level,
6+
string Message,
7+
string Category,
8+
string Member,
9+
string FilePath,
10+
int Line);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Options;
3+
4+
namespace Trion.Core.Logging;
5+
6+
public static class LoggerExtensions
7+
{
8+
public static IServiceCollection AddTrionLogger(
9+
this IServiceCollection services,
10+
Action<LoggerOptions>? configure = null)
11+
{
12+
if (configure != null) services.Configure(configure);
13+
return services.AddSingleton(sp =>
14+
{
15+
var opts = sp.GetRequiredService<IOptions<LoggerOptions>>().Value;
16+
return new TrionLogger(opts);
17+
});
18+
}
19+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace Trion.Core.Logging;
2+
3+
public sealed class LoggerOptions
4+
{
5+
public string Folder { get; set; } = "Logs";
6+
public bool WriteToConsole { get; set; } = true;
7+
public bool WriteToFile { get; set; } = true;
8+
public int DaysToCompress { get; set; } = 7;
9+
public int DaysToKeep { get; set; } = 30;
10+
11+
#if DEBUG
12+
public bool ShowInfo { get; set; } = true;
13+
public bool ShowSuccess { get; set; } = true;
14+
public bool ShowWarning { get; set; } = true;
15+
public bool ShowError { get; set; } = true;
16+
#else
17+
public bool ShowInfo { get; set; } = false;
18+
public bool ShowSuccess { get; set; } = false;
19+
public bool ShowWarning { get; set; } = true;
20+
public bool ShowError { get; set; } = true;
21+
#endif
22+
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
using System.Collections.Concurrent;
2+
using System.IO.Compression;
3+
using System.Runtime.CompilerServices;
4+
using System.Text;
5+
6+
7+
namespace Trion.Core.Logging;
8+
9+
public sealed class TrionLogger : IAsyncDisposable
10+
{
11+
private readonly LoggerOptions _opts;
12+
private readonly BlockingCollection<LogEntry> _queue = new();
13+
private readonly CancellationTokenSource _cts = new();
14+
private readonly Task _writerTask;
15+
private readonly SemaphoreSlim _fileGate = new(1, 1);
16+
17+
private StreamWriter? _todayWriter;
18+
private DateTime _todayDate;
19+
20+
public TrionLogger(LoggerOptions? opts = null)
21+
{
22+
_opts = opts ?? new LoggerOptions();
23+
Directory.CreateDirectory(_opts.Folder);
24+
OpenTodayFile();
25+
_writerTask = Task.Run(WritePump);
26+
_ = Task.Run(HouseKeepingLoop);
27+
}
28+
29+
/* ------------- public API ------------- */
30+
public void Info(string message, string category = "",
31+
[CallerMemberName] string member = "",
32+
[CallerFilePath] string file = "",
33+
[CallerLineNumber] int line = 0)
34+
=> Write(LogLevel.Info, message, category, member, file, line);
35+
36+
public void Success(string message, string category = "",
37+
[CallerMemberName] string member = "",
38+
[CallerFilePath] string file = "",
39+
[CallerLineNumber] int line = 0)
40+
=> Write(LogLevel.Success, message, category, member, file, line);
41+
42+
public void Warning(string message, string category = "",
43+
[CallerMemberName] string member = "",
44+
[CallerFilePath] string file = "",
45+
[CallerLineNumber] int line = 0)
46+
=> Write(LogLevel.Warning, message, category, member, file, line);
47+
48+
public void Error(string message, string category = "",
49+
[CallerMemberName] string member = "",
50+
[CallerFilePath] string file = "",
51+
[CallerLineNumber] int line = 0)
52+
=> Write(LogLevel.Error, message, category, member, file, line);
53+
54+
public void Error(Exception ex, string category = "",
55+
[CallerMemberName] string member = "",
56+
[CallerFilePath] string file = "",
57+
[CallerLineNumber] int line = 0)
58+
=> Error(ex.ToString(), category, member, file, line);
59+
60+
/* ------------- private ------------- */
61+
private void Write(LogLevel lvl, string msg, string cat,
62+
string member, string file, int line)
63+
{
64+
bool allowed = lvl switch
65+
{
66+
LogLevel.Info => _opts.ShowInfo,
67+
LogLevel.Success => _opts.ShowSuccess,
68+
LogLevel.Warning => _opts.ShowWarning,
69+
LogLevel.Error => _opts.ShowError,
70+
_ => false
71+
};
72+
73+
if (!allowed) return;
74+
75+
var entry = new LogEntry(
76+
Timestamp: DateTimeOffset.UtcNow,
77+
Level: lvl,
78+
Message: msg,
79+
Category: cat,
80+
Member: member,
81+
FilePath: Path.GetFileName(file),
82+
Line: line);
83+
84+
_queue.Add(entry);
85+
}
86+
87+
private async Task WritePump()
88+
{
89+
await Task.Run(() =>
90+
{
91+
foreach (var entry in _queue.GetConsumingEnumerable(_cts.Token))
92+
{
93+
var line = Format(entry);
94+
if (_opts.WriteToConsole) WriteConsole(line, entry.Level);
95+
if (_opts.WriteToFile) WriteFileAsync(line).GetAwaiter().GetResult();
96+
}
97+
}, _cts.Token);
98+
}
99+
100+
private static string Format(LogEntry e) =>
101+
$"{e.Timestamp:O}|{e.Level,-7}|{e.Category,-15}|{e.Member}({e.FilePath}:{e.Line})|{e.Message}";
102+
103+
private static void WriteConsole(string text, LogLevel lvl)
104+
{
105+
var color = lvl switch
106+
{
107+
LogLevel.Info => ConsoleColor.Cyan,
108+
LogLevel.Success => ConsoleColor.Green,
109+
LogLevel.Warning => ConsoleColor.Yellow,
110+
LogLevel.Error => ConsoleColor.Red,
111+
_ => ConsoleColor.Gray
112+
};
113+
lock (Console.Out)
114+
{
115+
var prev = Console.ForegroundColor;
116+
Console.ForegroundColor = color;
117+
Console.WriteLine(text);
118+
Console.ForegroundColor = prev;
119+
}
120+
}
121+
122+
private async ValueTask WriteFileAsync(string line)
123+
{
124+
await _fileGate.WaitAsync();
125+
try
126+
{
127+
var now = DateTime.UtcNow;
128+
if (now.Date != _todayDate)
129+
{
130+
_todayWriter?.Dispose();
131+
OpenTodayFile();
132+
}
133+
await _todayWriter!.WriteLineAsync(line);
134+
await _todayWriter.FlushAsync();
135+
}
136+
finally { _fileGate.Release(); }
137+
}
138+
139+
private void OpenTodayFile()
140+
{
141+
_todayDate = DateTime.UtcNow.Date;
142+
var path = Path.Combine(_opts.Folder, $"trion-{_todayDate:yyyyMMdd}.log");
143+
_todayWriter = new StreamWriter(path, append: true, Encoding.UTF8) { AutoFlush = false };
144+
}
145+
146+
private async Task HouseKeepingLoop()
147+
{
148+
while (!_cts.Token.IsCancellationRequested)
149+
{
150+
try
151+
{
152+
await Task.Delay(TimeSpan.FromHours(6), _cts.Token);
153+
HouseKeeping();
154+
}
155+
catch { /* best effort */ }
156+
}
157+
}
158+
159+
private void HouseKeeping()
160+
{
161+
var now = DateTime.UtcNow;
162+
var compressDate = now.AddDays(-_opts.DaysToCompress);
163+
var deleteDate = now.AddDays(-_opts.DaysToKeep);
164+
165+
foreach (var f in Directory.GetFiles(_opts.Folder, "trion-*.log"))
166+
{
167+
var fi = new FileInfo(f);
168+
if (fi.LastWriteTimeUtc < deleteDate)
169+
{
170+
fi.Delete();
171+
}
172+
else if (fi.LastWriteTimeUtc < compressDate &&
173+
!f.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
174+
{
175+
var gz = f + ".gz";
176+
if (!File.Exists(gz))
177+
{
178+
using var original = fi.OpenRead();
179+
using var compressed = File.Create(gz);
180+
using var gzip = new GZipStream(compressed, CompressionMode.Compress);
181+
original.CopyTo(gzip);
182+
}
183+
fi.Delete();
184+
}
185+
}
186+
}
187+
188+
public async ValueTask DisposeAsync()
189+
{
190+
_queue.CompleteAdding();
191+
_cts.Cancel();
192+
await _writerTask;
193+
_cts.Dispose();
194+
_queue.Dispose();
195+
await _fileGate.WaitAsync();
196+
_todayWriter?.Dispose();
197+
_fileGate.Dispose();
198+
}
199+
}

src/Trion.Core/Trion.Core.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,9 @@
1313
<Folder Include="Services\" />
1414
</ItemGroup>
1515

16+
<ItemGroup>
17+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
18+
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.9" />
19+
</ItemGroup>
20+
1621
</Project>

src/Trion.Desktop/Components/Pages/Home.razor

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,17 @@
33
<h1>Hello, world!</h1>
44

55
Welcome to your new app.
6+
7+
@inject TrionLogger Log
8+
9+
<Button class="btn btn-primary" @onclick="Fire">Log something</Button>
10+
11+
@code{
12+
private void Fire()
13+
{
14+
Log.Info("MAUI Hybrid info");
15+
Log.Success("MAUI Hybrid success");
16+
Log.Warning("MAUI Hybrid warning");
17+
Log.Error(new Exception("MAUI Hybrid error"));
18+
}
19+
}

src/Trion.Desktop/Components/_Imports.razor

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
@using Trion.Desktop.Components
1010
@using MudBlazor
1111
@using MudBlazor.Components
12-
@using MudBlazor.Services
12+
@using MudBlazor.Services
13+
@using Trion.Core.Logging

src/Trion.Desktop/MauiProgram.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.Extensions.Logging;
22
using MudBlazor.Services;
3+
using Trion.Core.Logging;
34

45
namespace Trion.Desktop
56
{
@@ -8,21 +9,33 @@ public static class MauiProgram
89
public static MauiApp CreateMauiApp()
910
{
1011
var builder = MauiApp.CreateBuilder();
11-
builder
12-
.UseMauiApp<App>()
12+
builder.UseMauiApp<App>()
1313
.ConfigureFonts(fonts =>
1414
{
1515
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
1616
});
1717

1818
// Add MudBlazor services
1919
builder.Services.AddMudServices();
20+
2021
builder.Services.AddMauiBlazorWebView();
2122

2223
#if DEBUG
2324
builder.Services.AddBlazorWebViewDeveloperTools();
2425
builder.Logging.AddDebug();
2526
#endif
27+
builder.Services.AddSingleton(new LoggerOptions
28+
{
29+
Folder = Path.Combine(FileSystem.AppDataDirectory, "Logs"),
30+
WriteToConsole = true,
31+
WriteToFile = true,
32+
ShowInfo = true,
33+
ShowSuccess = true,
34+
ShowWarning = true,
35+
ShowError = true
36+
});
37+
38+
builder.Services.AddTrionLogger();
2639

2740
return builder.Build();
2841
}

src/Trion.Desktop/Trion.Desktop.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,7 @@
4646
<ItemGroup>
4747
<PackageReference Include="MudBlazor" Version="8.*" />
4848
</ItemGroup>
49+
<ItemGroup>
50+
<ProjectReference Include="..\Trion.Core\Trion.Core.csproj" />
51+
</ItemGroup>
4952
</Project>

0 commit comments

Comments
 (0)