asp.net core mcroservices 架構之 分散式日誌(二)之自定義日誌開發
一 netcore日誌原理
netcore的日誌是作為一個擴充套件庫存在的,每個元件都有它的入口,那麼作為研究這個元件的入口是最好的,首先看兩種方式:
這個是原始碼例子提供的。
1 var loggingConfiguration = new ConfigurationBuilder() 2 .SetBasePath(Directory.GetCurrentDirectory()) 3 .AddJsonFile("View Codelogging.json", optional: false, reloadOnChange: true) 4 .Build(); 5 6 // A Web App based program would configure logging via the WebHostBuilder. 7 // Create a logger factory with filters that can be applied across all logger providers. 8 varserviceCollection = new ServiceCollection() 9 .AddLogging(builder => 10 { 11 builder 12 .AddConfiguration(loggingConfiguration.GetSection("Logging")) 13 .AddFilter("Microsoft", LogLevel.Debug) 14 .AddFilter("System", LogLevel.Debug) 15 .AddFilter("SampleApp.Program", LogLevel.Debug) 16 .AddConsole(); 17 #if NET461 18 builder.AddEventLog(); 19 #elif NETCOREAPP2_2 20 #else 21 #error Target framework needs to be updated 22 #endif 23 }); 24 25 // providers may be added to a LoggerFactory before any loggers are created 26 27 28 var serviceProvider = serviceCollection.BuildServiceProvider(); 29 // getting the logger using the class's name is conventional 30 _logger = serviceProvider.GetRequiredService<ILogger<Program>>();
這個是咱們使用hostbuild中的擴充套件
var host = new WebHostBuilder().ConfigureAppConfiguration((webHostBuild,configBuild) => { var env = webHostBuild.HostingEnvironment; configBuild.AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{env.EnvironmentName}.json" ,optional:true,reloadOnChange:true) .SetBasePath(Directory.GetCurrentDirectory()); }).ConfigureLogging((hostingContext, logging) => { logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")) .AddCustomizationLogger(); }).UseKestrel((hostcon,opt)=> { opt.ListenAnyIP(5555); }) .UseStartup<Startup>(); var ihost= host.Build(); ihost.Run();View Code
從以上兩種可以看出,其實第二種WebHostBuilder是封裝第一種的。所以咱們選擇從第一個入口著手。
netcore日誌設計思想是:LoggingBuider 構建,LoggerFactory和Logger類負責日誌操作和Log提供程式的管理,Configuration是配置功能。
那麼咱們基於以上的程式碼,看LoggingBuilder類
using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.Logging { internal class LoggingBuilder : ILoggingBuilder { public LoggingBuilder(IServiceCollection services) { Services = services; } public IServiceCollection Services { get; } } }
再看為Service做的擴充套件
using System; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection { /// <summary> /// Extension methods for setting up logging services in an <see cref="IServiceCollection" />. /// </summary> public static class LoggingServiceCollectionExtensions { /// <summary> /// Adds logging services to the specified <see cref="IServiceCollection" />. /// </summary> /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param> /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> public static IServiceCollection AddLogging(this IServiceCollection services) { return AddLogging(services, builder => { }); } /// <summary> /// Adds logging services to the specified <see cref="IServiceCollection" />. /// </summary> /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param> /// <param name="configure">The <see cref="ILoggingBuilder"/> configuration delegate.</param> /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> public static IServiceCollection AddLogging(this IServiceCollection services, Action<ILoggingBuilder> configure) { if (services == null) { throw new ArgumentNullException(nameof(services)); } services.AddOptions(); services.TryAdd(ServiceDescriptor.Singleton<ILoggerFactory, LoggerFactory>());//生成log的工廠 services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>))); //Log泛型類 services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<LoggerFilterOptions>>( new DefaultLoggerLevelConfigureOptions(LogLevel.Information)));//Filter配置類 configure(new LoggingBuilder(services)); //提供一個回掉方法,將logbuilder作為上下文傳入 return services; } } }
以上就是Log日誌部分完成相當於註冊功能的程式碼。
在入口中咱們看到了,回掉函式中放著載入配置和指定Logging提供程式。首先看載入配置:
builder.AddConfiguration(loggingConfiguration.GetSection("Logging")) AddConfiguration是在 Logging.Configuration中的LoggingBuilderExtensions.cs// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Configuration; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.Logging { /// <summary> /// Extension methods for setting up logging services in an <see cref="ILoggingBuilder" />. /// </summary> public static class LoggingBuilderExtensions { /// <summary> /// Configures <see cref="LoggerFilterOptions" /> from an instance of <see cref="IConfiguration" />. /// </summary> /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param> /// <param name="configuration">The <see cref="IConfiguration" /> to add.</param> /// <returns>The builder.</returns> public static ILoggingBuilder AddConfiguration(this ILoggingBuilder builder, IConfiguration configuration) { builder.AddConfiguration(); //這個是下面緊挨著程式碼塊的實現,主要是根據類名或者別名,找出對應的configuration並載入 builder.Services.AddSingleton<IConfigureOptions<LoggerFilterOptions>>
(new LoggerFilterConfigureOptions(configuration)); //新增filter結點的配置 builder.Services.AddSingleton<IOptionsChangeTokenSource<LoggerFilterOptions>>
(new ConfigurationChangeTokenSource<LoggerFilterOptions>(configuration));//更改後需要通知檢控類重新整理操作 builder.Services.AddSingleton(new LoggingConfiguration(configuration));//將這個配直節放入service return builder; } } }
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.Extensions.Logging.Configuration { /// <summary> /// Extension methods for setting up logging services in an <see cref="ILoggingBuilder" />. /// </summary> public static class LoggingBuilderConfigurationExtensions { /// <summary> /// 這個類就是新增根據型別或者是別名去找到配置節的功能 Adds services required to consume <see cref="ILoggerProviderConfigurationFactory"/> or <see cref="ILoggerProviderConfiguration{T}"/> /// </summary> public static void AddConfiguration(this ILoggingBuilder builder) { builder.Services.TryAddSingleton<ILoggerProviderConfigurationFactory
, LoggerProviderConfigurationFactory>(); builder.Services.TryAddSingleton(typeof(ILoggerProviderConfiguration<>)
, typeof(LoggerProviderConfiguration<>)); } } }
大家其實看到了,全部是往ServiceColl中扔東西,但是細想一下,這不就是根據部件組合出功能的思想嗎?不同的部件會組合出新的功能。那日誌是如何組合出來的?在咱們的入口類中有這一句:
_logger = serviceProvider.GetRequiredService<ILogger<Program>>();在上面為Service做擴充套件的那一段程式碼中已經為ILogger<>註冊了服務,還有LoggerFactory也是。那麼就從這個類入手,看序列圖:
我們知道netcore把DI集成了,基礎元件和業務類都可以作為服務來看待,然後進行控制服務以及服務的相互組合,比如在服務中
LoggerFactory類需要初始化,那麼看看它的建構函式:
public LoggerFactory() : this(Enumerable.Empty<ILoggerProvider>()) { } public LoggerFactory(IEnumerable<ILoggerProvider> providers) : this(providers, new StaticFilterOptionsMonitor(new LoggerFilterOptions())) { } public LoggerFactory(IEnumerable<ILoggerProvider> providers, LoggerFilterOptions filterOptions) : this(providers, new StaticFilterOptionsMonitor(filterOptions)) { } public LoggerFactory(IEnumerable<ILoggerProvider> providers, IOptionsMonitor<LoggerFilterOptions> filterOption) { foreach (var provider in providers) { AddProviderRegistration(provider, dispose: false); } _changeTokenRegistration = filterOption.OnChange(RefreshFilters); RefreshFilters(filterOption.CurrentValue); }
會不會奇怪為什麼建構函式中 ILoggerProvider是以列表的形式出現?
再看看console提供程式的服務註冊程式碼:
public static class ConsoleLoggerExtensions { /// <summary> /// Adds a console logger named 'Console' to the factory. /// </summary> /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param> public static ILoggingBuilder AddConsole(this ILoggingBuilder builder) { builder.AddConfiguration(); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, ConsoleLoggerProvider>()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<ConsoleLoggerOptions>, ConsoleLoggerOptionsSetup>()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IOptionsChangeTokenSource<ConsoleLoggerOptions>, LoggerProviderOptionsChangeTokenSource<ConsoleLoggerOptions, ConsoleLoggerProvider>>()); return builder; }
它是以 TryAddEnumerable 的形式新增的,也就是說 ILoggerProvider 在服務中有一個列表。那麼這個就清楚了,你可以新增
AddDebug AddConsole許多的提供程式,然後全部被注入進LoggerFacotry,LoggerFacotry會生成Logger然後傳遞給主Logger統一管理。
LoggerFactory有段程式碼 :private void SetLoggerInformation(ref LoggerInformation loggerInformation, ILoggerProvider provider, string categoryName) { loggerInformation.Logger = provider.CreateLogger(categoryName); loggerInformation.ProviderType = provider.GetType(); loggerInformation.ExternalScope = provider is ISupportExternalScope; } private LoggerInformation[] CreateLoggers(string categoryName) { var loggers = new LoggerInformation[_providerRegistrations.Count]; for (int i = 0; i < _providerRegistrations.Count; i++) { SetLoggerInformation(ref loggers[i], _providerRegistrations[i].Provider, categoryName); } ApplyRules(loggers, categoryName, 0, loggers.Length); return loggers; }
然後
public ILogger CreateLogger(string categoryName) { if (CheckDisposed()) { throw new ObjectDisposedException(nameof(LoggerFactory)); } lock (_sync) { if (!_loggers.TryGetValue(categoryName, out var logger)) { logger = new Logger(this) { Loggers = CreateLoggers(categoryName) //上面的程式碼塊生成 }; _loggers[categoryName] = logger; } return logger; } }
二 netcore自定義日誌開發
說了那麼多,咱們開始動手自己做一個。咱們並不是新建一個服務,而是為Logging服務做一個擴充套件,
所以不用新建builder部分,而是為LoggerBuilder新建一個擴充套件,先看程式碼結構:
然後看看為LoggerBuilder做的擴充套件方法:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Configuration; using Microsoft.Extensions.Logging.Console; using Microsoft.Extensions.Options; namespace Walt.Freamwork.Log { public static class CustomizationLoggerLoggerExtensions { /// <summary> /// Adds a console logger named 'Console' to the factory. /// </summary> /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param> public static ILoggingBuilder AddCustomizationLogger(this ILoggingBuilder builder) { builder.AddConfiguration(); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, CustomizationLoggerProvider>()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CustomizationLoggerOptions>, CustomizationLoggerOptionsSetup>()); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IOptionsChangeTokenSource<ConsoleLoggerOptions>, LoggerProviderOptionsChangeTokenSource<ConsoleLoggerOptions, ConsoleLoggerProvider>>()); return builder; } /// <summary> /// Adds a console logger named 'Console' to the factory. /// </summary> /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param> /// <param name="configure"></param> public static ILoggingBuilder AddCustomizationLogger(this ILoggingBuilder builder, Action<CustomizationLoggerOptions> configure) { if (configure == null) { throw new ArgumentNullException(nameof(configure)); } builder.AddCustomizationLogger(); builder.Services.Configure(configure); return builder; } } }
就像第一部分的原理,將你自己的CustomizationLoggerProvider服務新增進服務中,就完成了一半。配置檔案和配置Token,
有了這個token,就可以監控到配置檔案更改,從而引發change方法,讓開發去做一些事情。那麼看配置檔案:
{ "Logging": { "LogLevel": { "Default": "Debug", "System": "Debug", "Microsoft": "Debug" }, "KafkaLog":{ "Prix":"這是我的自定義日誌提供程式" } } }
再看看配置類:
就兩個引數,咱們配置了一個。再來看看配置安裝程式:
僅此而已,然後就是上面的擴充套件方法,給註冊就ok了。
如何用這些配置尼?看provider類
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Concurrent; using System.Collections.Generic; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console.Internal; using Microsoft.Extensions.Options; namespace Walt.Freamwork.Log { // IConsoleLoggerSettings is obsolete #pragma warning disable CS0618 // Type or member is obsolete [ProviderAlias("KafkaLog")] //還記得上面講原理是注入了providerconfiguration等類,就是根據這個別名去找配直節的。 public class CustomizationLoggerProvider : ILoggerProvider, ISupportExternalScope { private readonly ConcurrentDictionary<string, CustomizationLogger> _loggers = new ConcurrentDictionary<string, CustomizationLogger>(); private readonly Func<string, LogLevel, bool> _filter; private readonly CustomizationLoggerProcessor _messageQueue = new CustomizationLoggerProcessor(); private static readonly Func<string, LogLevel, bool> trueFilter = (cat, level) => true; private static readonly Func<string, LogLevel, bool> falseFilter = (cat, level) => false; private IDisposable _optionsReloadToken; private bool _includeScopes; private string _prix; private IExternalScopeProvider _scopeProvider; public CustomizationLoggerProvider(IOptionsMonitor<CustomizationLoggerOptions> options)
//這裡自動和configuration中的值繫結後,被注入到這裡來了。 { // Filter would be applied on LoggerFactory level _filter = trueFilter; _optionsReloadToken = options.OnChange(ReloadLoggerOptions); //這個就是我說的需要註冊token服務,然後配置更改就會激發這個方法 ReloadLoggerOptions(options.CurrentValue); } private void ReloadLoggerOptions(CustomizationLoggerOptions options) { _includeScopes = options.IncludeScopes; _prix=options.Prix; var scopeProvider = GetScopeProvider(); foreach (var logger in _loggers.Values) { logger.ScopeProvider = scopeProvider; } } private IEnumerable<string> GetKeyPrefixes(string name) { while (!string.IsNullOrEmpty(name)) { yield return name; var lastIndexOfDot = name.LastIndexOf('.'); if (lastIndexOfDot == -1) { yield return "Default"; break; } name = name.Substring(0, lastIndexOfDot); } } private IExternalScopeProvider GetScopeProvider() { if (_includeScopes && _scopeProvider == null) { _scopeProvider = new LoggerExternalScopeProvider(); } return _includeScopes ? _scopeProvider : null; } public void Dispose() { _optionsReloadToken?.Dispose(); _messageQueue.Dispose(); } public void SetScopeProvider(IExternalScopeProvider scopeProvider) { _scopeProvider = scopeProvider; } public ILogger CreateLogger(string name) { return _loggers.GetOrAdd(name, CreateLoggerImplementation); } private CustomizationLogger CreateLoggerImplementation(string name) { var includeScopes = _includeScopes; return new CustomizationLogger(name,null
,includeScopes? _scopeProvider: null,_messageQueue,_prix); //這裡就是你的終端類了,裡面實現為kafka發訊息或者寫到redis都行。 } } #pragma warning restore CS0618 }
下面打包然後上傳到nuget服務:
呼叫方檢視包
如果沒有這個包 dotnet add新增,如果有,直接把project專案檔案中的包版本改以下,restore就ok了。
下面進行呼叫:
執行:還記得配置檔案中的
這個就是用來做測試的,目前輸出還是用console:
執行結果:
customizationLogger的程式是從console那個提供程式中挖過來的,console中有很多的上一個版本的程式碼,而我肯定是需要新的,所以把Obsolete的程式碼全部刪除。
這是console的程式。
日誌第三部分講整合kafka,希望大家關注和討論。