using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; namespace Remotely.Shared.Services { public class FileLogger : ILogger { private static readonly ConcurrentQueue _logQueue = new(); private static readonly ConcurrentStack _scopeStack = new(); private static readonly SemaphoreSlim _writeLock = new(1, 1); private readonly string _categoryName; private readonly System.Timers.Timer _sinkTimer = new(5000) { AutoReset = false }; public FileLogger(string categoryName) { _categoryName = categoryName; _sinkTimer.Elapsed += SinkTimer_Elapsed; } private static string LogPath => Path.Combine(Path.GetTempPath(), "Remotely", "Logs", $"LogFile_{DateTime.Now:yyyy-MM-dd}.log"); public IDisposable BeginScope(TState state) { _scopeStack.Push($"{state}"); return new NoopDisposable(); } public bool IsEnabled(LogLevel logLevel) { switch (logLevel) { #if DEBUG case LogLevel.Trace: case LogLevel.Debug: return true; #endif case LogLevel.Information: case LogLevel.Warning: case LogLevel.Error: case LogLevel.Critical: return true; case LogLevel.None: default: return false; } } public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { try { var scopeStack = _scopeStack.Any() ? new string[] { _scopeStack.First(), _scopeStack.Last() } : Array.Empty(); var message = FormatLogEntry(logLevel, _categoryName, $"{state}", exception, scopeStack); _logQueue.Enqueue(message); _sinkTimer.Start(); } catch (Exception ex) { Console.WriteLine($"Error queueing log entry: {ex.Message}"); } } private static void CheckLogFileExists() { Directory.CreateDirectory(Path.GetDirectoryName(LogPath)!); if (!File.Exists(LogPath)) { File.Create(LogPath).Close(); } } private static string FormatLogEntry(LogLevel logLevel, string categoryName, string state, Exception exception, string[] scopeStack) { var ex = exception; var exMessage = exception?.Message; while (ex?.InnerException is not null) { exMessage += $" | {ex.InnerException.Message}"; ex = ex.InnerException; } var entry = $"[{logLevel}]\t" + $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t"; entry += scopeStack.Any() ? $"[{string.Join(" - ", scopeStack)} - {categoryName}]\t" : $"[{categoryName}]\t"; entry += $"Message: {state}\t"; if (!string.IsNullOrWhiteSpace(exMessage)) { entry += exMessage; } if (exception is not null) { entry += $"{Environment.NewLine}{exception.StackTrace}"; } entry += Environment.NewLine; return entry; } private async void SinkTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { try { await _writeLock.WaitAsync(); CheckLogFileExists(); var message = string.Empty; while (_logQueue.TryDequeue(out var entry)) { message += entry; } File.AppendAllText(LogPath, message); } catch (Exception ex) { Console.WriteLine($"Error writing log entry: {ex.Message}"); } finally { _writeLock.Release(); } } private class NoopDisposable : IDisposable { public void Dispose() { _scopeStack.TryPop(out _); } } } }