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提供程序。首先看加載配置:

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統一管理。

Add和TryAdd區別就是一個如果有相同的接口和實現,調用幾次就會有幾個服務,而后者不會,永遠只會創建一個服務。

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,希望大家關注和討論。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM