Files
meeting-assistant/MeetingAssistant/Logging/MeetingAssistantFileLoggerProvider.cs
codex 250d3b7a1e
PR and Push Build/Test / build-and-test (push) Successful in 16m43s
Generalize settings and logs assistant
2026-05-30 12:57:51 +02:00

193 lines
5.5 KiB
C#

using System.Collections.Concurrent;
using System.Text;
namespace MeetingAssistant.Logging;
public static class MeetingAssistantLogFiles
{
public const string CurrentLogFileName = "meeting-assistant.log";
public const int RetainedRotatedLogFiles = 4;
public static string DefaultLogDirectory =>
Path.Combine(Path.GetTempPath(), "MeetingAssistant", "Logs");
public static string CurrentLogPath(string? directory = null)
{
return Path.Combine(directory ?? DefaultLogDirectory, CurrentLogFileName);
}
public static string GetRotatedLogFileName(int index)
{
return $"{CurrentLogFileName}.{index}";
}
public static IReadOnlyList<string> EnumerateLogPaths(string? directory = null)
{
var root = directory ?? DefaultLogDirectory;
return Enumerable.Range(0, RetainedRotatedLogFiles + 1)
.Select(index => index == 0
? Path.Combine(root, CurrentLogFileName)
: Path.Combine(root, GetRotatedLogFileName(index)))
.Where(File.Exists)
.ToArray();
}
public static void Rotate(string? directory = null)
{
var root = directory ?? DefaultLogDirectory;
Directory.CreateDirectory(root);
try
{
var oldest = Path.Combine(root, GetRotatedLogFileName(RetainedRotatedLogFiles));
if (File.Exists(oldest))
{
File.Delete(oldest);
}
for (var index = RetainedRotatedLogFiles - 1; index >= 1; index--)
{
var source = Path.Combine(root, GetRotatedLogFileName(index));
if (!File.Exists(source))
{
continue;
}
var target = Path.Combine(root, GetRotatedLogFileName(index + 1));
File.Move(source, target, overwrite: true);
}
var current = Path.Combine(root, CurrentLogFileName);
if (File.Exists(current))
{
File.Move(current, Path.Combine(root, GetRotatedLogFileName(1)), overwrite: true);
}
}
catch (IOException)
{
// Another app instance or test host may still have the log open.
// Logging should keep working, so append to the existing current log.
}
}
}
public sealed class MeetingAssistantFileLoggerProvider : ILoggerProvider
{
private readonly ConcurrentDictionary<string, MeetingAssistantFileLogger> loggers = new(StringComparer.Ordinal);
private readonly object sync = new();
private readonly StreamWriter writer;
private bool disposed;
public MeetingAssistantFileLoggerProvider(string? directory = null)
{
var logDirectory = directory ?? MeetingAssistantLogFiles.DefaultLogDirectory;
MeetingAssistantLogFiles.Rotate(logDirectory);
var stream = new FileStream(
MeetingAssistantLogFiles.CurrentLogPath(logDirectory),
FileMode.Append,
FileAccess.Write,
FileShare.ReadWrite);
writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
{
AutoFlush = true
};
}
public ILogger CreateLogger(string categoryName)
{
return loggers.GetOrAdd(categoryName, name => new MeetingAssistantFileLogger(name, Write));
}
public void Dispose()
{
lock (sync)
{
if (disposed)
{
return;
}
disposed = true;
writer.Dispose();
}
}
private void Write(
string categoryName,
LogLevel logLevel,
EventId eventId,
string message,
Exception? exception)
{
if (disposed)
{
return;
}
lock (sync)
{
if (disposed)
{
return;
}
writer.Write(DateTimeOffset.Now.ToString("O"));
writer.Write(' ');
writer.Write(logLevel);
writer.Write(" [");
writer.Write(categoryName);
writer.Write(']');
if (eventId.Id != 0 || !string.IsNullOrWhiteSpace(eventId.Name))
{
writer.Write(" (");
writer.Write(eventId.Id);
if (!string.IsNullOrWhiteSpace(eventId.Name))
{
writer.Write(':');
writer.Write(eventId.Name);
}
writer.Write(')');
}
writer.Write(' ');
writer.WriteLine(message);
if (exception is not null)
{
writer.WriteLine(exception);
}
}
}
private sealed class MeetingAssistantFileLogger(
string categoryName,
Action<string, LogLevel, EventId, string, Exception?> write) : ILogger
{
public IDisposable? BeginScope<TState>(TState state)
where TState : notnull
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return logLevel != LogLevel.None;
}
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
write(categoryName, logLevel, eventId, formatter(state, exception), exception);
}
}
}