今天看AspNetCore源代碼發(fā)現(xiàn)日志模塊的設計模式(提供者模式),特此記錄
類圖 & 分析
角色分析
日志工廠 ( LoggerFactory --> ILoggerFactory)
- 提供注冊提供者
- 創(chuàng)建日志記錄器(Logger)
日志記錄器(Logger --> ILogger)
- 寫入日志記錄(遍歷所有日志提供者的Logger)
- 這里所有注冊的日志提供者聚合
日志提供者(ConsoleLoggerProvider --> ILoggerProvider)
- 創(chuàng)建具體日志記錄器
具體日志記錄者(ConsoleLogger赢赊,EventLogLogger)
- 將日志寫入具體媒介(控制臺棒呛,Windows事件日志)
現(xiàn)在來看看這個模式
- 提供標準的日志寫入接口(ILogger)
- 提供日志提供者接口(ILoggerProvider)
- 提供注冊提供者接口(ILoggerFactory.AddProvider)
這里只是列出部分類和方法,整個Logging要比這個還多域携,為什么寫個日志要整那么多東西簇秒?
程序唯一不會變就是不斷在變化,這個也是為什么要設計模式運用到程序當中的原因秀鞭,讓程序可擴展來應對這種變化趋观。
AspNetCore內置8種日志記錄提供程序,但肯定還是遠遠不夠锋边,因為有的可能想把日志寫在文本皱坛,有的想寫在Mongodb,有的想寫在ElasticSearch等等豆巨,Microsoft不可能把所有的都實現(xiàn)剩辟,就算實現(xiàn)也未必適合你的業(yè)務使用。
假設現(xiàn)在需要把日志寫在Mongo往扔,只需要
- 實現(xiàn)Mongodb的ILogger - 將日志寫到Mongodb
- 實現(xiàn)Mongodb的ILoggerProvider - 創(chuàng)建Mongodb的Logger
- 把Provider注冊到AspNetCore - ILoggerFactory.AddProvider
這里都是新增代碼達到實現(xiàn)把日志寫入到Mongodb贩猎,這就是6大設計原則之一對擴展開放(可以添加自己的日志),對修改封閉(不需要修改到內部的方法)
AspNetCore代碼實現(xiàn)(只列出接口)
ILoggerFactory
ILogger CreateLogger(string categoryName);
void AddProvider(ILoggerProvider provider);
CreateLogger
: 這個和ILoggerProvider提供的CreateLogger雖然都是現(xiàn)實ILogger接口萍膛,但是做的事情不一樣吭服,LoggerFactory創(chuàng)建的是Logger實例,里面聚合了具體寫日志的Logger蝗罗,遍歷它們輸出艇棕。
categoryName
: 可以指定具體,若使用泛型相當于typeof(T).FullName
串塑,這個用于篩選過濾日志
AddProvider
: 注冊一個新的提供者沼琉,然后遍歷現(xiàn)有的Logger,把新的Provider添加到現(xiàn)有l(wèi)ogger里面
ILoggerProvider
ILogger CreateLogger(string categoryName);
CreateLogger
: 用于創(chuàng)建具體寫日志Logger(例如Console)
ILogger
void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter);
bool IsEnabled(LogLevel logLevel);
IDisposable BeginScope<TState>(TState state);
Log<TState>(....)
: 輸出日志
bool IsEnabled
: 指定的日志級別是否可用
IDisposable BeginScope<TState>()
: 開啟日志作用域桩匪,將這個域范圍的日志都放一起
AspNetCore中使用Log4Net
AspNetCore使用Log4Net作為記錄很簡單打瘪,只需
- 安裝包:
dotnet install Microsoft.Extensions.Logging.Log4Net.AspNetCore
- Configure 添加:
loggerFactory.AddLog4Net();
- 添加
log4net.config
配置文件
看看Microsoft.Extensions.Logging.Log4Net.AspNetCore如何實現(xiàn)ILogger和ILoggerProvider接口
Log4NetProvider
public ILogger CreateLogger(string categoryName)
=> this.loggers.GetOrAdd(categoryName, this.CreateLoggerImplementation);
private Log4NetLogger CreateLoggerImplementation(string name)
{
var options = new Log4NetProviderOptions
{
Name = name,
LoggerRepository = this.loggerRepository.Name
};
options.ScopeFactory = new Log4NetScopeFactory(new Log4NetScopeRegistry());
return new Log4NetLogger(options);
}
Log4NetLogger
switch (logLevel)
{
case LogLevel.None:
break;
case LogLevel.Critical:
{
string overrideCriticalLevelWith = options.OverrideCriticalLevelWith;
if (!string.IsNullOrEmpty(overrideCriticalLevelWith) && overrideCriticalLevelWith.Equals(LogLevel.Critical.ToString(), StringComparison.OrdinalIgnoreCase))
{
log.Critical(text, exception);
}
else
{
log.Fatal(text, exception);
}
break;
}
case LogLevel.Debug:
log.Debug(text, exception);
break;
case LogLevel.Error:
log.Error(text, exception);
break;
case LogLevel.Information:
log.Info(text, exception);
break;
case LogLevel.Warning:
log.Warn(text, exception);
break;
case LogLevel.Trace:
log.Trace(text, exception);
break;
default:
log.Warn($"Encountered unknown log level {logLevel}, writing out as Info.");
log.Info(text, exception);
break;
}
log4net的ILog是沒有Trace和Critical方法,這兩個是擴展方法吸祟,調用log4net log4net.Repository.Hierarchy.Logger.Log()方法
log4net 里面有Fatal代表日志最高級別瑟慈,AspNetCore的Critical是日志最高級別,習慣log4net可能習慣用Fatal屋匕,這個時候只需要在注冊的時候
loggerFactory.AddLog4Net(new Log4NetProviderOptions()
{
OverrideCriticalLevelWith = "Critical"
});
在Controller調用
_logger.LogCritical("Log Critical");
看看效果
2020-04-27 13:42:05,042 [10] FATAL LoggingPattern.Controllers.WeatherForecastController (null) - Log Critical
奇怪,沒有按預期發(fā)生借杰。這個組件是開源的过吻,可以下載下來調試看看
github克隆下來 Microsoft.Extensions.Logging.Log4Net.AspNetCore
調試過程
- 將Microsoft.Extensions.Logging.Log4Net.AspNetCore.csproj的SignAssembly設置false(這個是程序集強簽名)
<SignAssembly>false</SignAssembly>
- 將引用改成引用本地,我這里是放在跟項目平級
<ItemGroup>
<ProjectReference Include="..\Microsoft.Extensions.Logging.Log4Net.AspNetCore\src\Microsoft.Extensions.Logging.Log4Net.AspNetCore\Microsoft.Extensions.Logging.Log4Net.AspNetCore.csproj" />
</ItemGroup>
我這里是用VSCode,如果用VS不用這么麻煩
-
然后就可以打斷點纤虽,在寫日志和之前看到的那個判斷打個斷點
接下來就是看看這個值怎么來的
builder.Services.AddSingleton<ILoggerProvider>(new Log4NetProvider(options));
public Log4NetProvider(Log4NetProviderOptions options)
{
}
注冊一個單例的Log4NetProvider,參入?yún)?shù)options逼纸,Logger是在Provider的CreateLogger創(chuàng)建洋措,現(xiàn)在看看CreateLogger
public ILogger CreateLogger(string categoryName)
=> this.loggers.GetOrAdd(categoryName, this.CreateLoggerImplementation);
private Log4NetLogger CreateLoggerImplementation(string name)
{
var options = new Log4NetProviderOptions
{
Name = name,
LoggerRepository = this.loggerRepository.Name
};
options.ScopeFactory = new Log4NetScopeFactory(new Log4NetScopeRegistry());
return new Log4NetLogger(options);
}
到這里就清楚了,CreateLoggerImplementation里面又new了一個options杰刽,然后沒有給OverrideCriticalLevelWith賦值(我認為這是個Bug菠发,應該也很少人會用這個功能)這里之所以沒用單例的options,因為要給每個Logger的目錄名稱動態(tài)賦值贺嫂。
給這個庫作者提了Issues和PR
添加自定義的日志記錄器
假設現(xiàn)在需要把日志加入到Mongodb
- 添加Mongodb驅動
dotnet add package MongoDB.Driver
- 實現(xiàn)接口ILogger
public class MongodbLogger : ILogger
{
private readonly string _name;
private MongoDB.Driver.IMongoDatabase _database;
public MongodbLogger(string name, MongoDB.Driver.IMongoDatabase database)
{
_name = name;
_database = database;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
var collection = _database.GetCollection<dynamic>(logLevel.ToString().ToLower());
string message = formatter(state, exception);
collection.InsertOneAsync(new
{
time = DateTime.Now,
name = _name,
message,
exception
});
}
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
public System.IDisposable BeginScope<TState>(TState state) => NullScope.Instance;
}
- 實現(xiàn)ILoggerProvider接口
public class MongodbProvider : ILoggerProvider
{
private readonly ConcurrentDictionary<string, MongodbLogger> _loggers = new ConcurrentDictionary<string, MongodbLogger>();
private MongoDB.Driver.IMongoDatabase _database;
public MongodbProvider(MongoDB.Driver.IMongoDatabase database)
{
_database = database;
}
public ILogger CreateLogger(string categoryName)
=> _loggers.GetOrAdd(categoryName, name => new MongodbLogger(categoryName, this._database));
public void Dispose() => this._loggers.Clear();
}
- 添加MongodbLogging擴展函數(shù)(非必須)
public static ILoggerFactory AddMongodb(this ILoggerFactory factory, string connetionString = "mongodb://127.0.0.1:27017/logging")
{
var mongoUrl = new MongoDB.Driver.MongoUrl(connetionString);
var client = new MongoDB.Driver.MongoClient(mongoUrl);
factory.AddProvider(new MongodbProvider(client.GetDatabase(mongoUrl.DatabaseName)));
return factory;
}
- Configure注冊MongodbLogging
loggerFactory.AddMongodb();
擴展
設計模式的好處是滓鸠,我們可以容易擴展它達到我們要求,除了要知道如何擴展它第喳,還應該在其他地方應用它糜俗,例如我們經常需要消息通知用戶,但是通知渠道(提供者)曲饱,在一開始未必全部知道悠抹,例如一開始只有短信,郵件通知扩淀,隨著業(yè)務發(fā)展可能需要增加微信推送锌钮,提供者模式就很好應對這一種情況,很容易畫出下面類圖引矩。
當需要擴展發(fā)送消息渠道梁丘,只需要實現(xiàn)ISenderProvider(哪個提供),ISender(如何發(fā)送)