CustomFileLoggerProvider.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. using Microsoft.Extensions.Logging;
  2. using System;
  3. using System.Collections.Concurrent;
  4. using System.Text;
  5. using System.Text.Encodings.Web;
  6. using System.Text.Json;
  7. using System.Text.Unicode;
  8. using System.Threading.Channels;
  9. namespace IES.ExamServer.DI
  10. {
  11. public class CustomFileLoggerProvider : ILoggerProvider
  12. {
  13. private readonly string _logDirectory;
  14. private readonly bool _enableConsoleOutput;
  15. private readonly string _timestamp;
  16. private readonly ConcurrentDictionary<string, CustomFileLogger> _loggers = new ConcurrentDictionary<string, CustomFileLogger>();
  17. public CustomFileLoggerProvider(string logDirectory, bool enableConsoleOutput)
  18. {
  19. _logDirectory = logDirectory;
  20. _enableConsoleOutput = enableConsoleOutput;
  21. _timestamp = DateTime.Now.ToString("yyMMddHH"); // 生成时间戳
  22. if (!Directory.Exists(_logDirectory))
  23. {
  24. Directory.CreateDirectory(_logDirectory);
  25. }
  26. }
  27. public ILogger CreateLogger(string categoryName)
  28. {
  29. return _loggers.GetOrAdd(categoryName, name => new CustomFileLogger(_logDirectory, name, _enableConsoleOutput, _timestamp));
  30. }
  31. public void Dispose()
  32. {
  33. _loggers.Clear();
  34. }
  35. }
  36. public class CustomFileLogger : ILogger
  37. {
  38. private readonly string _logDirectory;
  39. private readonly string _categoryName;
  40. private readonly bool _enableConsoleOutput;
  41. private static readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1);
  42. private readonly List<string> _logBuffer = new List<string>();
  43. // private readonly Timer _flushTimer;
  44. private readonly string _timestamp;
  45. private readonly Channel<string> _consoleLogChannel;
  46. private readonly Channel<string> _fileLogChannel;
  47. public CustomFileLogger(string logDirectory, string categoryName, bool enableConsoleOutput, string timestamp)
  48. {
  49. _logDirectory = logDirectory;
  50. _categoryName = categoryName;
  51. _enableConsoleOutput = enableConsoleOutput;
  52. // 每 5 秒刷新一次日志缓冲区
  53. // _flushTimer = new Timer(FlushLogs, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
  54. _timestamp=timestamp;
  55. // 创建无界 Channel
  56. _consoleLogChannel = Channel.CreateUnbounded<string>();
  57. // 创建无界 Channel
  58. _fileLogChannel = Channel.CreateUnbounded<string>();
  59. // 启动后台任务处理控制台日志
  60. _ = Task.Run(ProcessConsoleLogs);
  61. // 启动后台任务处理文件日志
  62. _ = Task.Run(ProcessFileLogs);
  63. }
  64. public IDisposable BeginScope<TState>(TState state) => null;
  65. public bool IsEnabled(LogLevel logLevel) => true;
  66. public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
  67. {
  68. var logMessage = formatter(state, exception);
  69. var logFileName = $"{logLevel}_{_timestamp}.log"; // 按时间戳命名文件
  70. var logFilePath = Path.Combine(_logDirectory, logFileName);
  71. // 异步写入文件
  72. // _ = WriteToFileAsync(logFilePath, logLevel, logMessage);
  73. // 将日志消息写入 Channel(不阻塞)
  74. _fileLogChannel.Writer.TryWrite($"{logFilePath}|||{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{logLevel}] {logMessage}{Environment.NewLine}");
  75. // 输出到控制台
  76. if (_enableConsoleOutput)
  77. {
  78. // Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{logLevel}] {logMessage}");
  79. // _ = Task.Run(() => Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{logLevel}] {logMessage}"));
  80. _consoleLogChannel.Writer.TryWrite($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{logLevel}] {logMessage}");
  81. }
  82. }
  83. private async Task ProcessConsoleLogs()
  84. {
  85. await foreach (var logMessage in _consoleLogChannel.Reader.ReadAllAsync())
  86. {
  87. Console.WriteLine(logMessage); // 输出到控制台
  88. }
  89. }
  90. private async Task ProcessFileLogs()
  91. {
  92. await foreach (var logEntry in _fileLogChannel.Reader.ReadAllAsync())
  93. {
  94. try
  95. {
  96. // 解析日志条目
  97. var parts = logEntry.Split("|||");
  98. var logFilePath = parts[0];
  99. var logMessage = parts[1];
  100. // 异步写入文件
  101. await File.AppendAllTextAsync(logFilePath, logMessage);
  102. }
  103. catch (Exception ex)
  104. {
  105. // 处理文件写入异常
  106. // Console.WriteLine($"Failed to write log to file: {ex.Message}");
  107. _consoleLogChannel.Writer.TryWrite($"Failed to write log to file: {ex.Message}");
  108. }
  109. }
  110. }
  111. //private async void FlushLogs(object state)
  112. //{
  113. // List<string> logsToWrite;
  114. // lock (_logBuffer)
  115. // {
  116. // if (_logBuffer.Count == 0) return;
  117. // logsToWrite = new List<string>(_logBuffer);
  118. // _logBuffer.Clear();
  119. // }
  120. // var logFileName = $"{_categoryName}_{_timestamp}.log";
  121. // var logFilePath = Path.Combine(_logDirectory, logFileName);
  122. // await _fileLock.WaitAsync(); // 获取锁
  123. // try
  124. // {
  125. // await File.AppendAllLinesAsync(logFilePath, logsToWrite);
  126. // }
  127. // finally
  128. // {
  129. // _fileLock.Release(); // 释放锁
  130. // }
  131. //}
  132. public void Dispose()
  133. {
  134. // _flushTimer?.Change(Timeout.Infinite, 0); // 停止定时器
  135. // FlushLogs(null); // 最后一次刷新日志
  136. }
  137. }
  138. public static class LoggerExtensions
  139. {
  140. //private static readonly SemaphoreSlim _dataFileLock = new SemaphoreSlim(1, 1);
  141. //private static readonly Channel<string> _fileLogChannel = Channel.CreateUnbounded<string>();
  142. //private static readonly Channel<string> _consoleLogChannel = Channel.CreateUnbounded<string>();
  143. private static readonly Channel<string> _fileLogChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(10000)
  144. {
  145. FullMode = BoundedChannelFullMode.DropWrite //
  146. });
  147. private static readonly Channel<string> _consoleLogChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(10000)
  148. {
  149. //DropOldest: 此模式会移除并丢弃通道中最旧的那个数据项,以便为正在写入的新数据腾出空间。通道里最早进入的数据会被舍弃,从而让新的数据可以进入通道。
  150. //DropNewest: 在这种模式下,为了给要写入的新数据项腾出空间,会移除并丢弃通道中最新的那个数据项。也就是说,新的数据会覆盖掉原本在通道里最新进入的元素,保证新数据能被写入。
  151. //Wait: 当使用这种模式时,如果尝试向已满的有界通道写入数据,调用写入操作的线程会等待,直到通道中有空间可用,然后再完成写入操作。这意味着线程会被阻塞,直到可以成功放入新的数据项。
  152. FullMode = BoundedChannelFullMode.DropWrite // 直接丢弃当前正要写入的这个数据项,通道中的内容保持不变,相当于放弃这次写入操作。
  153. });
  154. //懒加载
  155. private static readonly Lazy<Task> _fileLogProcessor = new Lazy<Task>(() => Task.Run(ProcessFileLogs));
  156. private static readonly Lazy<Task> _consoleLogProcessor = new Lazy<Task>(() => Task.Run(ProcessConsoleLogs));
  157. public static void LogData<T>(this ILogger logger, T data, string id)
  158. {
  159. // 确保后台任务已启动,现在就要用确保启动
  160. _ = _fileLogProcessor.Value;
  161. _ = _consoleLogProcessor.Value;
  162. var logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Logs", "DataLogs");
  163. if (!Directory.Exists(logDirectory))
  164. {
  165. Directory.CreateDirectory(logDirectory);
  166. }
  167. if (!string.IsNullOrWhiteSpace(id))
  168. {
  169. var logFilePath = Path.Combine(logDirectory, $"{id}.log");
  170. var logMessage = data?.ToJsonString();
  171. _fileLogChannel.Writer.TryWrite($"{logFilePath}|||{DateTime.Now:yyyy-MM-dd HH:mm:ss} [Data] {logMessage}{Environment.NewLine}");
  172. }
  173. else {
  174. _consoleLogChannel.Writer.TryWrite($"日志id为空,日志数据:{data?.ToJsonString()}");
  175. }
  176. //_ = Task.Run(async () =>
  177. //{
  178. // await _dataFileLock.WaitAsync(); // 获取锁
  179. // try
  180. // {
  181. // await File.AppendAllTextAsync(logFilePath, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [Data] {logMessage}{Environment.NewLine}");
  182. // }
  183. // finally
  184. // {
  185. // _dataFileLock.Release(); // 释放锁
  186. // }
  187. //});
  188. }
  189. static async Task ProcessFileLogs()
  190. {
  191. try
  192. {
  193. await foreach (var logEntry in _fileLogChannel.Reader.ReadAllAsync())
  194. {
  195. try
  196. {
  197. // 解析日志条目
  198. var parts = logEntry.Split("|||");
  199. var logFilePath = parts[0];
  200. var logMessage = parts[1];
  201. // 异步写入文件
  202. await File.AppendAllTextAsync(logFilePath, logMessage, Encoding.UTF8);
  203. }
  204. catch (Exception ex)
  205. {
  206. // 处理文件写入异常
  207. // Console.WriteLine($"Failed to write log to file: {ex.Message}");
  208. _consoleLogChannel.Writer.TryWrite($"Failed to write log to file: {ex.Message}");
  209. }
  210. }
  211. }
  212. catch (Exception ex) {
  213. _consoleLogChannel.Writer.TryWrite($"Failed to write log to file: {ex.Message}");
  214. }
  215. // 循环结束后,检查是否有剩余的日志条目
  216. }
  217. static async Task ProcessConsoleLogs()
  218. {
  219. await foreach (var logMessage in _consoleLogChannel.Reader.ReadAllAsync())
  220. {
  221. Console.WriteLine(logMessage); // 输出到控制台
  222. }
  223. }
  224. }
  225. }