Public Access
Generalize settings and logs assistant
PR and Push Build/Test / build-and-test (push) Successful in 16m43s
PR and Push Build/Test / build-and-test (push) Successful in 16m43s
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user