動(dòng)手造輪子:實(shí)現(xiàn)一個(gè)簡(jiǎn)單的基于 Console 的日志輸出

Intro
之前結(jié)合了微軟的 Logging 框架和 Serilog 寫(xiě)了一個(gè)簡(jiǎn)單的日志框架,但是之前的用法都是基于 log4net、serilog 的,沒(méi)有真正自己實(shí)現(xiàn)一個(gè)日志輸出,比如 Console、文件、數(shù)據(jù)庫(kù)、ES等,關(guān)于日志框架的設(shè)計(jì)可以參考之前的文章 動(dòng)手造輪子:寫(xiě)一個(gè)日志框架
實(shí)現(xiàn)思路
把日志放在一個(gè)隊(duì)列中,通過(guò)隊(duì)列方式慢慢的寫(xiě),避免并發(fā)問(wèn)題,同時(shí)異步寫(xiě)到 Console 避免因?yàn)閷?xiě)日志阻塞主線程的執(zhí)行
輸出的格式如何定義呢,像 log4net/nlog/serilog 這些都會(huì)支持自定義日志輸出格式,所以我們可以設(shè)計(jì)一個(gè)接口,實(shí)現(xiàn)一個(gè)默認(rèn)日志格式,當(dāng)用戶自定義日志格式的時(shí)候就使用用戶自定義的日志格式
針對(duì)不同的日志級(jí)別的日志應(yīng)該使用不同的顏色來(lái)輸出以方便尋找不同級(jí)別的日志
使用示例
來(lái)看一個(gè)使用的示例:
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");
日志輸出如下:

默認(rèn)的日志格式是 JSON 字符串,因?yàn)槲矣X(jué)得 JSON 更加結(jié)構(gòu)化,也會(huì)比較方便的去 PATCH 和日志分析,微軟的 Logging 框架也是在 .NET 5.0 中加入了 JsonConsoleFormatter,可以直接輸出 JSON 到控制臺(tái),如果需要也可以自定義一個(gè) Formatter 來(lái)實(shí)現(xiàn)自定義的格式化
實(shí)現(xiàn)源碼
使用 IConsoleLogFormatter 接口來(lái)自定義日志格式化
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);
}
}
實(shí)現(xiàn)的代碼比較簡(jiǎn)單,隊(duì)列的話使用了 BlockingCollection 來(lái)實(shí)現(xiàn)了一個(gè)內(nèi)存中的隊(duì)列
ConsoleLoggingProvider實(shí)現(xiàn)如下:
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 message
try
{
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
};
}
}
為了方便使用和更好的訪問(wèn)控制,上面的 ConsoleLoggingProvider 聲明成了 internal 并不直接對(duì)外開(kāi)放,并且定義了下面的擴(kuò)展方法來(lái)使用:
public static ILogHelperLoggingBuilder AddConsole(this ILogHelperLoggingBuilder loggingBuilder, IConsoleLogFormatter? consoleLogFormatter = null)
{
loggingBuilder.AddProvider(new ConsoleLoggingProvider(
consoleLogFormatter ?? new DefaultConsoleLogFormatter()));
return loggingBuilder;
}
DelegateFormatter
需要自定義的 Console 日志的格式的時(shí)候就實(shí)現(xiàn)一個(gè) IConsoleLogFormatter 來(lái)實(shí)現(xiàn)自己的格式化邏輯就可以了,不想手寫(xiě)一個(gè)類?也可以實(shí)現(xiàn)一個(gè) Func<LogHelperLoggingEvent, string> 委托,內(nèi)部會(huì)把委托轉(zhuǎn)成一個(gè) IConsoleLogFormatter,實(shí)現(xiàn)如下:
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);
}
擴(kuò)展方法:
public static ILogHelperLoggingBuilder AddConsole(this ILogHelperLoggingBuilder loggingBuilder, Func<LogHelperLoggingEvent, string> formatter)
{
loggingBuilder.AddProvider(new ConsoleLoggingProvider(new DelegateConsoleLogFormatter(formatter)));
return loggingBuilder;
}
More
在寫(xiě)一些小應(yīng)用的時(shí)候,經(jīng)常會(huì)遇到這樣的場(chǎng)景,就是執(zhí)行一個(gè)方法的時(shí)候包一層 try...catch,在發(fā)生異常時(shí)輸出異常信息,稍微包裝了一個(gè)
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);
}
}
原來(lái)想突出顯示錯(cuò)誤信息的時(shí)候,我會(huì)特別設(shè)置一個(gè) Console 的顏色以便方便的查看,原來(lái)會(huì)這樣設(shè)置,之前的 gRPC 示例項(xiàng)目原來(lái)就是這樣做的:
InvokeHelper.OnInvokeException = ex =>
{
var originalColor = ForegroundColor;
ForegroundColor = ConsoleColor.Red;
WriteLine(ex);
ForegroundColor = originalColor;
};
有了 Console logging 之后,我就可以把上面的委托默認(rèn)設(shè)置為 Log 一個(gè) Error(OnInvokeException = ex => LogHelper.GetLogger(typeof(InvokeHelper)).Error(ex);),只需要配置 Logging 使用 Console 輸出就可以了,也可以設(shè)置日志級(jí)別忽略一些不太需要的日志
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
【推薦】.NET Core開(kāi)發(fā)實(shí)戰(zhàn)視頻課程 ★★★
.NET Core實(shí)戰(zhàn)項(xiàng)目之CMS 第一章 入門(mén)篇-開(kāi)篇及總體規(guī)劃
【.NET Core微服務(wù)實(shí)戰(zhàn)-統(tǒng)一身份認(rèn)證】開(kāi)篇及目錄索引
Redis基本使用及百億數(shù)據(jù)量中的使用技巧分享(附視頻地址及觀看指南)
.NET Core中的一個(gè)接口多種實(shí)現(xiàn)的依賴注入與動(dòng)態(tài)選擇看這篇就夠了
10個(gè)小技巧助您寫(xiě)出高性能的ASP.NET Core代碼
用abp vNext快速開(kāi)發(fā)Quartz.NET定時(shí)任務(wù)管理界面
在ASP.NET Core中創(chuàng)建基于Quartz.NET托管服務(wù)輕松實(shí)現(xiàn)作業(yè)調(diào)度
現(xiàn)身說(shuō)法:實(shí)際業(yè)務(wù)出發(fā)分析百億數(shù)據(jù)量下的多表查詢優(yōu)化
