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 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);
了
有一股核电站发电的感觉,最终目的永远也逃不开烧开水