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.");}
}
我们主要关注ILogger
与ILoggerProvider
即可,由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 completedif (!Enqueue(message)){WriteMessage(message);}}// internal for testinginternal void WriteMessage(LogMessageEntry entry){try{IConsole console = entry.LogAsError ? ErrorConsole : Console;console.Write(entry.Message);}catch{CompleteAdding();}}
最终,通过队列削峰,走到了WriteMessage,最后就是最朴素无华的
cw console.Write(entry.Message);
了
有一股核电站发电的感觉,最终目的永远也逃不开烧开水