asp.net core mcroservices 架构之 分布式日志(二)之自定义日志开发
asp.net core mcroservices 架构之 分布式日志(二)之自定义日志开发
一 netcore日志原理
netcore的日志是作为一个扩展库存在的,每个组件都有它的入口,那么作为研究这个组件的入口是最好的,首先看两种方式:
这个是源码例子提供的。
- 1 var loggingConfiguration = new ConfigurationBuilder()
- 2 .SetBasePath(Directory.GetCurrentDirectory())
- 3 .AddJsonFile("logging.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 var serviceCollection = 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>>();
View Code
这个是咱们使用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提供程序。首先看加载配置:
- // 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统一管理。
- 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,希望大家关注和讨论。