动手造轮子:实现一个简单的基于 Console 的日志输出
Intro
之前结合了微软的 Logging 框架和 Serilog
写了一个简单的日志框架,但是之前的用法都是基于 log4net、serilog 的,没有真正自己实现一个日志输出,比如 Console
、文件、数据库、ES等,关于日志框架的设计可以参考之前的文章 动手造轮子:写一个日志框架
实现思路
把日志放在一个队列中,通过队列方式慢慢的写,避免并发问题,同时异步写到 Console 避免因为写日志阻塞主线程的执行
输出的格式如何定义呢,像 log4net/nlog/serilog 这些都会支持自定义日志输出格式,所以我们可以设计一个接口,实现一个默认日志格式,当用户自定义日志格式的时候就使用用户自定义的日志格式
针对不同的日志级别的日志应该使用不同的颜色来输出以方便寻找不同级别的日志
使用示例
来看一个使用的示例:
LogHelper.ConfigureLogging(builder =>
{builder.AddConsole()//.AddLog4Net()//.AddSerilog(loggerConfig => loggerConfig.WriteTo.Console())//.WithMinimumLevel(LogHelperLogLevel.Info)//.WithFilter((category, level) => level > LogHelperLogLevel.Error && category.StartsWith("System"))//.EnrichWithProperty("Entry0", ApplicationHelper.ApplicationName)//.EnrichWithProperty("Entry1", ApplicationHelper.ApplicationName, e => e.LogLevel >= LogHelperLogLevel.Error);
});var abc = "1233";
var logger = LogHelper.GetLogger<LoggerTest>();
logger.Debug("12333 {abc}", abc);
logger.Trace("122334334");
logger.Info($"122334334 {abc}");logger.Warn("12333, err:{err}", "hahaha");
logger.Error("122334334");
logger.Fatal("12333");
日志输出如下:
默认的日志格式是 JSON 字符串,因为我觉得 JSON 更加结构化,也会比较方便的去 PATCH 和日志分析,微软的 Logging 框架也是在 .NET 5.0 中加入了 JsonConsoleFormatter
,可以直接输出 JSON 到控制台,如果需要也可以自定义一个 Formatter 来实现自定义的格式化
实现源码
使用 IConsoleLogFormatter
接口来自定义日志格式化
public interface IConsoleLogFormatter
{string FormatAsString(LogHelperLoggingEvent loggingEvent);
}internal sealed class DefaultConsoleLogFormatter : IConsoleLogFormatter
{private static readonly JsonSerializerSettings _serializerSettings = new(){Converters ={new StringEnumConverter()},ReferenceLoopHandling = ReferenceLoopHandling.Ignore,};public string FormatAsString(LogHelperLoggingEvent loggingEvent){return loggingEvent.ToJson(_serializerSettings);}
}
实现的代码比较简单,队列的话使用了 BlockingCollection
来实现了一个内存中的队列
ConsoleLoggingProvider
实现如下:
internal sealed class ConsoleLoggingProvider : ILogHelperProvider
{private readonly IConsoleLogFormatter _formatter;private readonly BlockingCollection<LogHelperLoggingEvent> _messageQueue = new();private readonly Thread _outputThread;public ConsoleLoggingProvider(IConsoleLogFormatter formatter){_formatter = formatter;// Start Console message queue processor_outputThread = new Thread(ProcessLogQueue){IsBackground = true,Name = "Console logger queue processing thread"};_outputThread.Start();}public void EnqueueMessage(LogHelperLoggingEvent message){if (!_messageQueue.IsAddingCompleted){try{_messageQueue.Add(message);return;}catch (InvalidOperationException) { }}// Adding is completed so just log the messagetry{WriteLoggingEvent(message);}catch (Exception){// ignored}}public void Log(LogHelperLoggingEvent loggingEvent){EnqueueMessage(loggingEvent);}private void ProcessLogQueue(){try{foreach (LogHelperLoggingEvent message in _messageQueue.GetConsumingEnumerable()){WriteLoggingEvent(message);}}catch{try{_messageQueue.CompleteAdding();}catch{// ignored}}}private void WriteLoggingEvent(LogHelperLoggingEvent loggingEvent){try{var originalColor = Console.ForegroundColor;try{var log = _formatter.FormatAsString(loggingEvent);var logLevelColor = GetLogLevelConsoleColor(loggingEvent.LogLevel);Console.ForegroundColor = logLevelColor.GetValueOrDefault(originalColor);if (loggingEvent.LogLevel == LogHelperLogLevel.Error|| loggingEvent.LogLevel == LogHelperLogLevel.Fatal){Console.Error.WriteLine(log);}else{Console.WriteLine(log);}}catch (Exception ex){Console.WriteLine(ex);}finally{Console.ForegroundColor = originalColor;}}catch{Console.WriteLine(loggingEvent.ToJson());}}private static ConsoleColor? GetLogLevelConsoleColor(LogHelperLogLevel logLevel){return logLevel switch{LogHelperLogLevel.Trace => ConsoleColor.Gray,LogHelperLogLevel.Debug => ConsoleColor.Gray,LogHelperLogLevel.Info => ConsoleColor.DarkGreen,LogHelperLogLevel.Warn => ConsoleColor.Yellow,LogHelperLogLevel.Error => ConsoleColor.Red,LogHelperLogLevel.Fatal => ConsoleColor.DarkRed,_ => null};}
}
为了方便使用和更好的访问控制,上面的 ConsoleLoggingProvider
声明成了 internal
并不直接对外开放,并且定义了下面的扩展方法来使用:
public static ILogHelperLoggingBuilder AddConsole(this ILogHelperLoggingBuilder loggingBuilder, IConsoleLogFormatter? consoleLogFormatter = null)
{loggingBuilder.AddProvider(new ConsoleLoggingProvider(consoleLogFormatter ?? new DefaultConsoleLogFormatter()));return loggingBuilder;
}
DelegateFormatter
需要自定义的 Console 日志的格式的时候就实现一个 IConsoleLogFormatter
来实现自己的格式化逻辑就可以了,不想手写一个类?也可以实现一个 Func<LogHelperLoggingEvent, string>
委托,内部会把委托转成一个 IConsoleLogFormatter
,实现如下:
internal sealed class DelegateConsoleLogFormatter : IConsoleLogFormatter
{private readonly Func<LogHelperLoggingEvent, string> _formatter;public DelegateConsoleLogFormatter(Func<LogHelperLoggingEvent, string> formatter){_formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));}public string FormatAsString(LogHelperLoggingEvent loggingEvent) => _formatter(loggingEvent);
}
扩展方法:
public static ILogHelperLoggingBuilder AddConsole(this ILogHelperLoggingBuilder loggingBuilder, Func<LogHelperLoggingEvent, string> formatter)
{loggingBuilder.AddProvider(new ConsoleLoggingProvider(new DelegateConsoleLogFormatter(formatter)));return loggingBuilder;
}
More
在写一些小应用的时候,经常会遇到这样的场景,就是执行一个方法的时候包一层 try...catch,在发生异常时输出异常信息,稍微包装了一个
public static Action<Exception>? OnInvokeException { get; set; }public static void TryInvoke(Action action)
{Guard.NotNull(action, nameof(action));try{action();}catch (Exception ex){OnInvokeException?.Invoke(ex);}
}
原来想突出显示错误信息的时候,我会特别设置一个 Console 的颜色以便方便的查看,原来会这样设置,之前的 gRPC 示例项目原来就是这样做的:
InvokeHelper.OnInvokeException = ex =>
{var originalColor = ForegroundColor;ForegroundColor = ConsoleColor.Red;WriteLine(ex);ForegroundColor = originalColor;
};
有了 Console logging 之后,我就可以把上面的委托默认设置为 Log 一个 Error(OnInvokeException = ex => LogHelper.GetLogger(typeof(InvokeHelper)).Error(ex);
),只需要配置 Logging 使用 Console 输出就可以了,也可以设置日志级别忽略一些不太需要的日志
LogHelper.ConfigureLogging(x=>x.AddConsole().WithMinimumLevel(LogHelperLogLevel.Info));
References
https://github.com/WeihanLi/WeihanLi.Common
https://github.com/WeihanLi/WeihanLi.Common/blob/dev/src/WeihanLi.Common/Logging/ConsoleLoggingProvider.cs