Public Access
193 lines
5.5 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|