Skip to content

Commit ef58e24

Browse files
authored
Merge pull request #17 from DecSmith42/feat/log-level-filename-config
feat: Add per-level filename functionality
2 parents c16c57b + e8a942d commit ef58e24

File tree

10 files changed

+176
-79
lines changed

10 files changed

+176
-79
lines changed

DecSm.Extensions.Logging.File.UnitTests/BasicTests.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ public sealed class BasicTests : TestBase
77
public void SetUp()
88
{
99
FileSystem = new();
10+
DirectFileLoggerProvider.FileSystem = FileSystem;
1011
BufferedFileLoggerProvider.FileSystem = FileSystem;
1112

1213
TimeProvider = new();
14+
DirectFileLoggerProvider.TimeProvider = TimeProvider;
1315
BufferedFileLoggerProvider.TimeProvider = TimeProvider;
1416
}
1517

@@ -61,4 +63,58 @@ public void Logger_Respects_Rooted_Config_Path()
6163
6264
"""));
6365
}
66+
67+
[TestCase(true)]
68+
[TestCase(false)]
69+
public void Logger_Logs_To_Level_Defined_File(bool buffered)
70+
{
71+
// Arrange
72+
var dbgLogPath = GetLogPath(null, $"{AppDomain.CurrentDomain.FriendlyName}_DBG");
73+
var errLogPath = GetLogPath(null, $"{AppDomain.CurrentDomain.FriendlyName}_ERR");
74+
75+
var logger = CreateBuilderWithLogger<BasicTests>(config =>
76+
{
77+
config.PerLevelLogName = new()
78+
{
79+
{ LogLevel.Trace, $"{AppDomain.CurrentDomain.FriendlyName}_DBG" },
80+
{ LogLevel.Debug, $"{AppDomain.CurrentDomain.FriendlyName}_DBG" },
81+
{ LogLevel.Information, $"{AppDomain.CurrentDomain.FriendlyName}_DBG" },
82+
{ LogLevel.Warning, $"{AppDomain.CurrentDomain.FriendlyName}_DBG" },
83+
{ LogLevel.Error, $"{AppDomain.CurrentDomain.FriendlyName}_ERR" },
84+
{ LogLevel.Critical, $"{AppDomain.CurrentDomain.FriendlyName}_ERR" },
85+
};
86+
},
87+
buffered);
88+
89+
// Act
90+
logger.LogTrace("Hello, world!");
91+
logger.LogDebug("Hello, world!");
92+
logger.LogInformation("Hello, world!");
93+
logger.LogWarning("This is an error message.");
94+
logger.LogError("This is an error message.");
95+
logger.LogCritical("This is an error message.");
96+
StopApp();
97+
98+
// Assert
99+
FileSystem.ShouldSatisfyAllConditions(fs => fs.File.Exists(dbgLogPath),
100+
fs => fs
101+
.File
102+
.ReadAllText(dbgLogPath)
103+
.ShouldBe("""
104+
[2020-01-01 11:00:00.000 +11:00 TRC DecSm.Extensions.Logging.File.UnitTests.BasicTests] Hello, world!
105+
[2020-01-01 11:00:00.000 +11:00 DBG DecSm.Extensions.Logging.File.UnitTests.BasicTests] Hello, world!
106+
[2020-01-01 11:00:00.000 +11:00 INF DecSm.Extensions.Logging.File.UnitTests.BasicTests] Hello, world!
107+
[2020-01-01 11:00:00.000 +11:00 WRN DecSm.Extensions.Logging.File.UnitTests.BasicTests] This is an error message.
108+
109+
"""),
110+
fs => fs.File.Exists(errLogPath),
111+
fs => fs
112+
.File
113+
.ReadAllText(errLogPath)
114+
.ShouldBe("""
115+
[2020-01-01 11:00:00.000 +11:00 ERR DecSm.Extensions.Logging.File.UnitTests.BasicTests] This is an error message.
116+
[2020-01-01 11:00:00.000 +11:00 CRT DecSm.Extensions.Logging.File.UnitTests.BasicTests] This is an error message.
117+
118+
"""));
119+
}
64120
}

DecSm.Extensions.Logging.File.UnitTests/TestBase.cs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,42 @@
22

33
public abstract class TestBase
44
{
5-
protected IDisposable? DisposableApp;
5+
private IDisposable? _disposableApp;
66
protected MockFileSystem FileSystem = null!;
77
protected TestTimeProvider TimeProvider = null!;
88

9-
protected string GetLogPath(string? timestamp = null) =>
9+
protected string GetLogPath(string? timestamp = null, string? customName = null) =>
1010
FileSystem.Path.Combine(FileSystem.Directory.GetCurrentDirectory(),
1111
"Logs",
1212
timestamp is not null
13-
? $"{AppDomain.CurrentDomain.FriendlyName}_{timestamp}.log"
14-
: $"{AppDomain.CurrentDomain.FriendlyName}.log");
15-
16-
protected ILogger CreateBuilderWithLogger<T>(Action<FileLoggerConfiguration>? configure = null)
13+
? customName is not null
14+
? $"{customName}_{timestamp}.log"
15+
: $"{AppDomain.CurrentDomain.FriendlyName}_{timestamp}.log"
16+
: customName is not null
17+
? $"{customName}.log"
18+
: $"{AppDomain.CurrentDomain.FriendlyName}.log");
19+
20+
protected ILogger CreateBuilderWithLogger<T>(Action<FileLoggerConfiguration>? configure = null, bool buffered = true)
1721
{
1822
var builder = Host.CreateApplicationBuilder();
1923
builder.Logging.ClearProviders();
2024
builder.Logging.SetMinimumLevel(LogLevel.Trace);
2125

2226
if (configure is not null)
23-
builder.Logging.AddFile(configure);
27+
builder.Logging.AddFile(configure, buffered);
2428
else
25-
builder.Logging.AddFile();
29+
builder.Logging.AddFile(buffered);
2630

2731
var app = builder.Build();
2832

29-
DisposableApp = app;
33+
_disposableApp = app;
3034

3135
return app.Services.GetRequiredService<ILogger<T>>();
3236
}
3337

3438
protected void StopApp()
3539
{
3640
Thread.Sleep(100);
37-
DisposableApp?.Dispose();
41+
_disposableApp?.Dispose();
3842
}
3943
}

DecSm.Extensions.Logging.File/Configuration/FileLoggerConfiguration.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public sealed class FileLoggerConfiguration
1717

1818
public string? LogName { get; set; } = DefaultLogName;
1919

20+
public Dictionary<LogLevel, string?> PerLevelLogName { get; set; } = [];
21+
2022
public long FileSizeLimitBytes { get; set; } = DefaultFileSizeLimitBytes;
2123

2224
public FileRolloverInterval RolloverInterval { get; set; } = DefaultRollingInterval;

DecSm.Extensions.Logging.File/FileLogger.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,6 @@ public void Log<TState>(
4242

4343
var log = $"[{now} {logLevelCode} {name}] {logMessage}{Environment.NewLine}";
4444

45-
logWriter.Log(log);
45+
logWriter.Log(log, logLevel);
4646
}
4747
}

DecSm.Extensions.Logging.File/Provider/BufferedFileLoggerProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace DecSm.Extensions.Logging.File.Provider;
22

3+
[PublicAPI]
34
[ProviderAlias("File")]
45
internal sealed class BufferedFileLoggerProvider(IOptionsMonitor<FileLoggerConfiguration> config)
56
: FileLoggerProvider(config), ILoggerProvider

DecSm.Extensions.Logging.File/Provider/DirectFileLoggerProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace DecSm.Extensions.Logging.File.Provider;
22

3+
[PublicAPI]
34
[ProviderAlias("File")]
45
internal sealed class DirectFileLoggerProvider(IOptionsMonitor<FileLoggerConfiguration> config)
56
: FileLoggerProvider(config), ILoggerProvider

DecSm.Extensions.Logging.File/Provider/FileLoggerProvider.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ public ILogger CreateLogger(string categoryName)
1818
{
1919
LogWriter.Start();
2020

21+
#if NET8_0_OR_GREATER
22+
return _loggers.GetOrAdd(categoryName, name => new(name, LogWriter));
23+
#else
2124
return _loggers.GetOrAdd(categoryName, name => new(name, LogWriter))!;
25+
#endif
2226
}
2327

2428
protected FileLoggerConfiguration GetCurrentConfig() =>

DecSm.Extensions.Logging.File/Writer/BufferedFileLogWriter.cs

Lines changed: 86 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ internal sealed class BufferedFileLogWriter(
66
Func<FileLoggerConfiguration> getCurrentConfig
77
) : IFileLogWriter
88
{
9-
private readonly Channel<string> _logEntryChannel = Channel.CreateUnbounded<string>(new()
9+
private readonly Channel<LogEvent> _logEntryChannel = Channel.CreateUnbounded<LogEvent>(new()
1010
{
1111
SingleReader = true,
1212
SingleWriter = false,
@@ -31,15 +31,15 @@ public void Start()
3131
_writerThread.Start();
3232
}
3333

34-
public void Log(string log)
34+
public void Log(string log, LogLevel logLevel)
3535
{
3636
var attempt = 0;
3737

3838
while (true)
3939
{
4040
try
4141
{
42-
_logEntryChannel.Writer.TryWrite(log);
42+
_logEntryChannel.Writer.TryWrite(new(log, logLevel));
4343

4444
break;
4545
}
@@ -63,103 +63,124 @@ public void Dispose()
6363
}
6464

6565
private static void RunBackgroundThread(
66-
ChannelReader<string> reader,
66+
ChannelReader<LogEvent> reader,
6767
IFileSystem fileSystem,
6868
TimeProvider timeProvider,
6969
Func<FileLoggerConfiguration> getCurrentConfig,
7070
CancellationToken cancellationToken)
7171
{
7272
while (!cancellationToken.IsCancellationRequested)
7373
{
74-
var config = getCurrentConfig()!;
74+
var logsByLevel = new Dictionary<LogLevel, List<string>>();
75+
var logsLengthBytesByLevel = new Dictionary<LogLevel, int>();
7576

76-
var logs = new List<string>();
77-
var logsLengthBytes = 0;
77+
#if NET8_0_OR_GREATER
78+
var config = getCurrentConfig();
79+
#else
80+
var config = getCurrentConfig()!;
81+
#endif
7882

7983
var readCount = 0;
8084
const int maxReadCount = 10;
8185

8286
while (reader.TryRead(out var item) && readCount < maxReadCount)
8387
{
84-
logs.Add(item);
85-
logsLengthBytes += Encoding.UTF8.GetByteCount(item);
88+
logsByLevel.TryAdd(item.LogLevel, []);
89+
90+
logsByLevel[item.LogLevel]
91+
.Add(item.Message);
92+
93+
logsLengthBytesByLevel.TryAdd(item.LogLevel, 0);
94+
logsLengthBytesByLevel[item.LogLevel] += Encoding.UTF8.GetByteCount(item.Message);
95+
8696
readCount++;
8797
}
8898

89-
if (logs.Count == 0)
99+
if (logsByLevel.Count == 0)
90100
continue;
91101

92-
var attempt = 0;
93-
94-
while (true)
102+
foreach (var logLevel in logsByLevel.Keys)
95103
{
96-
try
97-
{
98-
var logsDirectoryName = config.LogDirectory;
99-
100-
var logsDirectory = fileSystem.Path.IsPathRooted(logsDirectoryName)
101-
? logsDirectoryName
102-
: fileSystem.Path.Combine(fileSystem.Directory.GetCurrentDirectory(), logsDirectoryName);
103-
104-
if (!fileSystem.Directory.Exists(logsDirectory))
105-
fileSystem.Directory.CreateDirectory(logsDirectory);
106-
107-
var logName = config.LogName ?? AppDomain.CurrentDomain.FriendlyName;
108-
var logFilePath = fileSystem.Path.Combine(logsDirectory, $"{logName}.log");
109-
110-
var fileInfo = fileSystem.FileInfo.New(logFilePath);
111-
var newFileCreated = !fileInfo.Exists;
104+
var logs = logsByLevel[logLevel];
105+
var logsLengthBytes = logsLengthBytesByLevel[logLevel];
112106

113-
if (!newFileCreated && fileInfo.Length + logsLengthBytes >= config.FileSizeLimitBytes)
114-
{
115-
FileLogWriterUtil.RollOnFileSize(fileSystem, timeProvider, logsDirectory, logName, logFilePath);
116-
newFileCreated = true;
117-
}
107+
var logsDirectoryName = config.LogDirectory;
118108

119-
if (!newFileCreated && config.RolloverInterval is not FileRolloverInterval.Infinite)
120-
newFileCreated = FileLogWriterUtil.RollOnTimeInterval(fileSystem,
121-
timeProvider,
122-
config.RolloverInterval,
123-
fileInfo,
124-
logsDirectory,
125-
logName,
126-
logFilePath);
109+
var logsDirectory = fileSystem.Path.IsPathRooted(logsDirectoryName)
110+
? logsDirectoryName
111+
: fileSystem.Path.Combine(fileSystem.Directory.GetCurrentDirectory(), logsDirectoryName);
127112

128-
if (newFileCreated)
129-
FileLogWriterUtil.PurgeOnTotalSize(fileSystem, config.MaxTotalSizeBytes, logsDirectory, logName);
113+
if (!fileSystem.Directory.Exists(logsDirectory))
114+
fileSystem.Directory.CreateDirectory(logsDirectory);
130115

131-
FileLogWriterUtil.WriteToFile(fileSystem, logFilePath, logs);
116+
var logName = config.PerLevelLogName.TryGetValue(logLevel, out var name)
117+
? name ?? AppDomain.CurrentDomain.FriendlyName
118+
: config.LogName ?? AppDomain.CurrentDomain.FriendlyName;
132119

133-
// If we have rolled over the file or are writing for the first time, we want to ensure the
134-
// file has the correct timestamps
135-
if (newFileCreated)
136-
{
137-
fileInfo.Refresh();
120+
var logFilePath = fileSystem.Path.Combine(logsDirectory, $"{logName}.log");
138121

139-
fileInfo.CreationTimeUtc = fileInfo.LastWriteTimeUtc = fileInfo.LastAccessTimeUtc = timeProvider.GetUtcNow()
140-
.DateTime;
141-
}
122+
var attempt = 0;
142123

143-
break;
144-
}
145-
catch (Exception ex)
124+
while (true)
146125
{
147126
try
148127
{
149-
Console.WriteLine(ex);
150-
Debug.WriteLine(ex);
128+
var fileInfo = fileSystem.FileInfo.New(logFilePath);
129+
var newFileCreated = !fileInfo.Exists;
130+
131+
if (!newFileCreated && fileInfo.Length + logsLengthBytes >= config.FileSizeLimitBytes)
132+
{
133+
FileLogWriterUtil.RollOnFileSize(fileSystem, timeProvider, logsDirectory, logName, logFilePath);
134+
newFileCreated = true;
135+
}
136+
137+
if (!newFileCreated && config.RolloverInterval is not FileRolloverInterval.Infinite)
138+
newFileCreated = FileLogWriterUtil.RollOnTimeInterval(fileSystem,
139+
timeProvider,
140+
config.RolloverInterval,
141+
fileInfo,
142+
logsDirectory,
143+
logName,
144+
logFilePath);
145+
146+
if (newFileCreated)
147+
FileLogWriterUtil.PurgeOnTotalSize(fileSystem, config.MaxTotalSizeBytes, logsDirectory, logName);
148+
149+
FileLogWriterUtil.WriteToFile(fileSystem, logFilePath, logs);
150+
151+
// If we have rolled over the file or are writing for the first time, we want to ensure the
152+
// file has the correct timestamps
153+
if (newFileCreated)
154+
{
155+
fileInfo.Refresh();
156+
157+
fileInfo.CreationTimeUtc = fileInfo.LastWriteTimeUtc = fileInfo.LastAccessTimeUtc = timeProvider.GetUtcNow()
158+
.DateTime;
159+
}
160+
161+
break;
151162
}
152-
catch
163+
catch (Exception ex)
153164
{
154-
// Can't do anything more here, better to just continue
165+
try
166+
{
167+
Console.WriteLine(ex);
168+
Debug.WriteLine(ex);
169+
}
170+
catch
171+
{
172+
// Can't do anything more here, better to just continue
173+
}
174+
175+
if (attempt >= 5)
176+
break;
177+
178+
attempt++;
155179
}
156-
157-
if (attempt >= 5)
158-
break;
159-
160-
attempt++;
161180
}
162181
}
163182
}
164183
}
184+
185+
private sealed record LogEvent(string Message, LogLevel LogLevel);
165186
}

0 commit comments

Comments
 (0)