【橙子老哥】.NetCore ILogger日志源码剖析解读

hello,大家好,又又又到了橙子老哥的分享时间,希望大家一起学习,一起进步。

欢迎加入.net意社区,第一时间了解我们的动态,地址:ccnetcore.com

今天,我们来玩一玩ILogger日志,废话少说,我们直接开始

1、扩展

相信大家看到ILogger,肯定有一种亲切感

var log=app.Services.GetRequiredService<ILogger<Program>>();
log.LogInformation("输出日志");

网上资料也很多,因为,我就不介绍它的用法了(其实也没什么用法,就是打印日志),不过可以看看它是怎么做到自定义扩展的

步骤 1: 创建自定义日志记录器
首先,你需要创建一个自定义日志记录器类。例如,我们可以创建一个名为 MyLogger 的类:

using Microsoft.Extensions.Logging;

public class MyLogger : ILogger
{
    private readonly string _name;

    public MyLogger(string name)
    {
        _name = name;
    }

    public IDisposable BeginScope<TState>(TState state) => null;

    public bool IsEnabled(LogLevel logLevel) => true;

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        if (formatter == null) return;

        var message = formatter(state, exception);
        // 这里添加你想要的逻辑,例如将日志记录到文件或数据库
        Console.WriteLine($"{logLevel}: {_name}: {message}");
    }
}

步骤 2: 创建自定义日志提供程序
接下来,我们需要创建一个自定义日志提供程序,用于创建我们的自定义日志记录器实例:

using Microsoft.Extensions.Logging;

public class MyLoggerProvider : ILoggerProvider
{
    public ILogger CreateLogger(string categoryName)
    {
        return new MyLogger(categoryName);
    }

    public void Dispose() { }
}

步骤 3: 将自定义日志提供程序添加到服务容器
接下来,在 Startup.cs 的 ConfigureServices 方法中,注册我们的自定义日志提供程序:

public void ConfigureServices(IServiceCollection services)
{
    // 其他服务注册...

    services.AddLogging(builder =>
    {
        builder.ClearProviders(); // 可选:清除默认提供程序
        builder.AddProvider(new MyLoggerProvider());
    });
}

步骤 4: 使用自定义日志记录器
现在您可以在您的应用程序中使用自定义日志记录器。例如,在控制器中:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

[ApiController]
[Route("[controller]")]
public class SampleController : ControllerBase
{
    private readonly ILogger<SampleController> _logger;

    public SampleController(ILogger<SampleController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IActionResult Get()
    {
        _logger.LogInformation("This is a custom log message.");
        return Ok("Check the logs for the custom message.");
    }
}

我们主要关注ILoggerILoggerProvider即可,由ILoggerProvider去创建ILogger

2、原理

接下来,我要看看,.net是如何实现这种扩展
我们从入口,开始,就是 _logger.LogInformation,看看他里面在做什么

        public static void LogInformation(this ILogger logger, string? message, params object?[] args)
        {
            logger.Log(LogLevel.Information, message, args);
        }

LogInformation 本质上就是个扩展方法,目的是调用logger中的Log方法,而日志等级,就是一个枚举而已

我们进一步看看:

  		public class Logger<T> : ILogger<T>
  		
        private readonly ILogger _logger;
       	void ILogger.Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
        {
            _logger.Log(logLevel, eventId, state, exception, formatter);
        }

走到泛型的实现,然后又调用了非泛型的ILogger,这个ILogger,不就是我们要扩展的日志组件吗 ?

到这里,我们研究下ILogger是怎么知道我们用哪个实现的呢?我们往上看看

        private readonly ILogger _logger;

        /// <summary>
        /// Creates a new <see cref="Logger{T}"/>.
        /// </summary>
        /// <param name="factory">The factory.</param>
        public Logger(ILoggerFactory factory)
        {
            ThrowHelper.ThrowIfNull(factory);

            _logger = factory.CreateLogger(GetCategoryName());
        }

可以看出,在构造函数的时候,通过ILoggerFactory去找到对应的Iogger的,而GetCategoryName就是传入的泛型类名

现在我们探究下,ILoggerFactory如何是通过类名去找到对应的ILogger

    public ILogger CreateLogger(string categoryName)
    {
      if (this.CheckDisposed())
        throw new ObjectDisposedException(nameof (LoggerFactory));
      Logger logger1;
      if (!this._loggers.TryGetValue(categoryName, out logger1))
      {
        lock (this._sync)
        {
          if (!this._loggers.TryGetValue(categoryName, out logger1))
          {
            logger1 = new Logger(categoryName, this.CreateLoggers(categoryName));
            Logger logger2 = logger1;
            Logger logger3 = logger1;
            (MessageLogger[] MessageLoggers, ScopeLogger[] ScopeLoggers) tuple = this.ApplyFilters(logger1.Loggers);
            MessageLogger[] messageLoggers = tuple.MessageLoggers;
            logger2.MessageLoggers = messageLoggers;
            logger3.ScopeLoggers = tuple.ScopeLoggers;
            this._loggers[categoryName] = logger1;
          }
        }
      }
      return (ILogger) logger1;
    }

这里有一个经典单例双锁模式,可以学习学习,防止锁的堵塞

继续走了一层,通过categoryName去找LoggerInformation[]

        private LoggerInformation[] CreateLoggers(string categoryName)
        {
            var loggers = new LoggerInformation[_providerRegistrations.Count];
            for (int i = 0; i < _providerRegistrations.Count; i++)
            {
                loggers[i] = new LoggerInformation(_providerRegistrations[i].Provider, categoryName);
            }
            return loggers;
        }

这里源头,又转到_providerRegistrations去了,通过这个

 private readonly List<ProviderRegistration> _providerRegistrations = new List<ProviderRegistration>();

去获取到的日志提供者Provider

这里就有一个疑惑了,这个对象也不是个静态变量,每次都去获取,没有地方赋值,不会出问题吗?

那我们顺着这个问题,去找找哪里给他赋值的,同时找找为什么不是静态字段为什么能每次查询到数据

 private void AddProviderRegistration(ILoggerProvider provider, bool dispose)
        {
            _providerRegistrations.Add(new ProviderRegistration
            {
                Provider = provider,
                ShouldDispose = dispose
            });

            if (provider is ISupportExternalScope supportsExternalScope)
            {
                _scopeProvider ??= new LoggerFactoryScopeProvider(_factoryOptions.ActivityTrackingOptions);

                supportsExternalScope.SetScopeProvider(_scopeProvider);
            }
        }

可以看到,在初始化的时候,会把ILoggerProvider 传递给它,类似初始化一次,我们再看看哪里进行调用这个的

 public void AddProvider(ILoggerProvider provider)
        {
            if (CheckDisposed())
            {
                throw new ObjectDisposedException(nameof(LoggerFactory));
            }

            ThrowHelper.ThrowIfNull(provider);

            lock (_sync)
            {
                AddProviderRegistration(provider, dispose: true);

                foreach (KeyValuePair<string, Logger> existingLogger in _loggers)
                {
                    Logger logger = existingLogger.Value;
                    LoggerInformation[] loggerInformation = logger.Loggers;

                    int newLoggerIndex = loggerInformation.Length;
                    Array.Resize(ref loggerInformation, loggerInformation.Length + 1);
                    loggerInformation[newLoggerIndex] = new LoggerInformation(provider, existingLogger.Key);

                    logger.Loggers = loggerInformation;
                    (logger.MessageLoggers, logger.ScopeLoggers) = ApplyFilters(logger.Loggers);
                }
            }
        }

看到这个,调用了AddProviderRegistration(provider, dispose: true);方法,真相大白了,我们通过ILoggerProvider的AddProvider方法,将自己的日志组件添加到了List _providerRegistrations 这个结合里面,然后打印日志的时候,会通过泛型的名称找到每一个日志提供者并缓存,通过日志提供者去找到它对应的logger去打印消息

另外一个问题,List _providerRegistrations不是静态的,为什么没有丢数据?

我们知道,类的创建不一定只是new了,可以通过ioc依赖注入,我们看看他的类,LoggerFactory是怎么创建的

        public static IServiceCollection AddLogging(this IServiceCollection services, Action<ILoggingBuilder> configure)
        {
            ThrowHelper.ThrowIfNull(services);

            services.AddOptions();

            services.TryAdd(ServiceDescriptor.Singleton<ILoggerFactory, LoggerFactory>());
            services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>)));

            services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<LoggerFilterOptions>>(
                new DefaultLoggerLevelConfigureOptions(LogLevel.Information)));

            configure(new LoggingBuilder(services));
            return services;
        }

很好,走到AddLogging这里,基本形成了一个闭环,他的注册是Singleton单例的,所以值一直不会丢失,就这个道理,其实这个也是一个很常见的操作

至此,闭环完成
1、AddLogging添加基础组件
2、AddProvider(ILoggerProvider provider)添加对应的ILoggerProvider 组件
2、ILogger<> 通过ProviderRegistration去找到ILoggerProvider
4、通过ILoggerProvider 去创建非泛型的ILogger
5、非泛型的ILogger进行输出打印,走我们之前AddProvider里扩展的ILogger实现

所有日志处理的逻辑,就都被抽象到了我们的实现中,ILoggerProvider 和ILogger;

3、默认实现

即时我们不自定义日志组件,它也是会控制台打印的,这是因为,它内置的默认的日志组件扩展
ConsoleLogger

  /// <inheritdoc />
        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }

            ThrowHelper.ThrowIfNull(formatter);

            t_stringWriter ??= new StringWriter();
            LogEntry<TState> logEntry = new LogEntry<TState>(logLevel, _name, eventId, state, exception, formatter);
            Formatter.Write(in logEntry, ScopeProvider, t_stringWriter);

            var sb = t_stringWriter.GetStringBuilder();
            if (sb.Length == 0)
            {
                return;
            }
            string computedAnsiString = sb.ToString();
            sb.Clear();
            if (sb.Capacity > 1024)
            {
                sb.Capacity = 1024;
            }
            _queueProcessor.EnqueueMessage(new LogMessageEntry(computedAnsiString, logAsError: logLevel >= Options.LogToStandardErrorThreshold));
        }

可以看出,默认的控制台打印,也不是直接cw,而是队列转接了一层_queueProcessor.EnqueueMessage

 public virtual void EnqueueMessage(LogMessageEntry message)
        {
            // cannot enqueue when adding is completed
            if (!Enqueue(message))
            {
                WriteMessage(message);
            }
        }
        // internal for testing
        internal void WriteMessage(LogMessageEntry entry)
        {
            try
            {
                IConsole console = entry.LogAsError ? ErrorConsole : Console;
                console.Write(entry.Message);
            }
            catch
            {
                CompleteAdding();
            }
        }

最终,通过队列削峰,走到了WriteMessage,最后就是最朴素无华的
cw console.Write(entry.Message);

有一股核电站发电的感觉,最终目的永远也逃不开烧开水

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值