动手造轮子:实现一个简单的基于 Console 的日志输出

动手造轮子:实现一个简单的基于 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");

日志输出如下:

log output

默认的日志格式是 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 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
        };
    }
}

为了方便使用和更好的访问控制,上面的 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));

diff

References

  • https://github.com/WeihanLi/WeihanLi.Common

  • https://github.com/WeihanLi/WeihanLi.Common/blob/dev/src/WeihanLi.Common/Logging/ConsoleLoggingProvider.cs

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值