首頁>技術>

快速上手

新增日誌提供程式

在文章主機(Host)中,講到Host.CreateDefaultBuilder方法,預設透過呼叫ConfigureLogging方法添加了ConsoleDebugEventSourceEventLog(僅Windows)共四種日誌記錄提供程式(Logger Provider),然後在主機Build過程中,透過AddLogging()註冊了日誌相關的服務。

csharp

.ConfigureLogging((hostingContext, logging) =>{    bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);    if (isWindows)    {        logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);    }    // 新增 Logging 配置    logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));        // ConsoleLoggerProvider    logging.AddConsole();    // DebugLoggerProvider    logging.AddDebug();    // EventSourceLoggerProvider    logging.AddEventSourceLogger();    if (isWindows)    {        // 在Windows平臺上,新增 EventLogLoggerProvider        logging.AddEventLog();    }    logging.Configure(options =>    {        options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId                                            | ActivityTrackingOptions.TraceId                                            | ActivityTrackingOptions.ParentId;    });})public class HostBuilder : IHostBuilder{    private void CreateServiceProvider()    {        var services = new ServiceCollection();                // ...                services.AddLogging();            // ...    }}

如果不想使用預設新增的日誌提供程式,我們可以透過ClearProviders清除所有已新增的日誌記錄提供程式,然後新增自己想要的,如Console

csharp

public static IHostBuilder CreateHostBuilder(string[] args) =>    Host.CreateDefaultBuilder(args)        .ConfigureLogging(logging =>        {            logging.ClearProviders()                .AddConsole();        })        .ConfigureWebHostDefaults(webBuilder =>        {            webBuilder.UseStartup<Startup>();        });

記錄日誌

日誌記錄提供程式均實現了介面ILoggerProvider,該介面可以建立ILogger例項。

透過注入服務ILogger<TCategoryName>,就可以非常方便的進行日誌記錄了。

該服務需要指定日誌的類別,可以是任意字串,但是我們約定使用所屬類的名稱,透過泛型體現。例如,在控制器ValuesController中,日誌類別就是ValuesController類的完全限定型別名。

csharp

public class ValuesController : ControllerBase{    private readonly ILogger<ValuesController> _logger;    public ValuesController(ILogger<ValuesController> logger)    {        _logger = logger;    }    [HttpGet]    public string Get()    {        _logger.LogInformation("ValuesController.Get");        return "Ok";    }}

當請求Get方法後,你就可以在控制檯中看到看到輸出的“ValuesController.Get”

如果你想要顯式指定日誌類別,則可以使用ILoggerFactory.CreateLogger方法:

csharp

public class ValuesController : ControllerBase{    private readonly ILogger _logger1;    public ValuesController(ILoggerFactory loggerFactory)    {        _logger1 = loggerFactory.CreateLogger("MyCategory");    }}

配置日誌

預設模板中,日誌的配置如下(在appsettings.{Environment}.json檔案中):

json

{  "Logging": {    "LogLevel": {      "Default": "Information",      "Microsoft": "Warning",      "Microsoft.Hosting.Lifetime": "Information"    }  }}

針對所有日誌記錄提供程式進行配置

LogLevel,顧名思義,就是指要記錄的日誌的最低級別(即要記錄大於等於該級別的日誌),想必大家都不陌生。下方會詳細介紹日誌級別。

LogLevel中的欄位,如上面示例中的“Default”、“Microsoft”等,表示日誌的類別,也就是咱們上面注入ILogger時指定的泛型引數。可以為每種類別設定記錄的最小日誌級別,也就是這些類別所對應的值。

下面詳細解釋一下示例中的三種日誌類別。

Default

預設情況下,如果分類沒有進行特別配置(即沒有在LogLevel中配置),則應用Default的配置。

Microsoft

所有分類以Microsoft開頭的日誌均應用Microsoft的配置。例如,Microsoft.AspNetCore.Routing.EndpointMiddleware類別的日誌就會應用該配置。

Microsoft.Hosting.Lifetime

所有分類以Microsoft.Hosting.Lifetime開頭的日誌均應用Microsoft.Hosting.Lifetime的配置。例如,分類Microsoft.Hosting.Lifetime就會應用該配置,而不會應用Microsoft,因為Microsoft.Hosting.LifetimeMicrosoft更具體。

OK,以上三種日誌類別就說這些了。

回到示例,你可能沒有注意到,這裡面沒有針對某個日誌記錄提供程式進行單獨配置(如:Console只記錄Error及以上級別日誌,而EventSource則需要記錄記錄所有級別日誌)。像這種,如果沒有針對特定的日誌記錄提供程式進行配置,則該配置將會應用到所有日誌記錄提供程式。

Windows EventLog 除外。EventLog必須顯式地進行配置,否則會使用其預設的LogLevel.Warning

針對指定的日誌記錄提供程式進行配置

接下來看一下如何針對指定的日誌記錄提供程式進行配置,先上示例:

json

{  "Logging": {    "LogLevel": {      "Default": "Information",      "Microsoft": "Warning",      "Microsoft.Hosting.Lifetime": "Information"    },    "Console": {      "LogLevel": {        "Default": "Error"      }    },    "Debug": {      "LogLevel": {        "Microsoft": "None"      }    },    "EventSource": {      "LogLevel": {        "Default": "Trace",        "Microsoft": "Trace",        "Microsoft.Hosting.Lifetime": "Trace"      }    }  }}

就像appsettings.{Environment}.jsonappsettings.json之間的關係一樣,Logging.{Provider}.LogLevel中的配置將會覆蓋Logging.LogLevel中的配置。

例如Logging.Console.LogLevel.Default將會覆蓋Logging.LogLevel.DefaultConsole日誌記錄器將預設記錄Error及其以上級別的日誌。

剛才提到了,Windows EventLog比較特殊,它不會繼承Logging.LogLevel的配置。EventLog預設日誌級別為LogLevel.Warning,如果想要修改,則必須顯式進行指定,如:

json

{  "Logging": {    "EventLog": {      "LogLevel": {        "Default": "Information"      }    }  }}

配置的篩選原理

當建立ILogger<TCategoryName>的物件例項時,ILoggerFactory根據不同的日誌記錄提供程式,將會:

查詢匹配該日誌記錄提供程式的配置。如果找不到,則使用通用配置。然後匹配擁有最長字首的配置類別。如果找不到,則使用Default配置。如果匹配到了多條配置,則採用最後一條。如果沒有匹配到任何配置,則使用MinimumLevel,這是個配置項,預設是LogLevel.Information

可以在ConfigureLogging擴充套件中使用SetMinimumLevel方法設定MinimumLevel

Log Level

日誌級別指示了日誌的嚴重程度,一共分為7等,從輕到重為(最後的None較為特殊):

日誌級別

描述

Trace

0

追蹤級別,包含最詳細的資訊。這些資訊可能包含敏感資料,預設情況下是禁用的,並且絕不能出現在生產環境中。

Debug

1

除錯級別,用於開發人員開發和除錯。資訊量一般比較大,在生產環境中一定要慎用。

Information

2

資訊級別,該級別平時使用較多。

Warning

3

警告級別,一些意外的事件,但這些事件並不對導致程式出錯。

Error

4

錯誤級別,一些無法處理的錯誤或異常,這些事件會導致當前操作或請求失敗,但不會導致整個應用出錯。

Critical

5

致命錯誤級別,這些錯誤會導致整個應用出錯。例如記憶體不足等。

None

6

指示不記錄任何日誌

日誌記錄提供程式

Console

日誌將輸出到控制檯中。

Debug

日誌將透過System.Diagnostics.Debug類進行輸出,可以透過VS輸出視窗檢視。

在 Linux 上,可以在/var/log/message/var/log/syslog下找到

EventSource

跨平臺日誌記錄,在Windows上則使用 ETW

Windows EventLog

僅在Windows系統下生效,可透過“事件檢視器”進行日誌檢視。

預設情況下

LogName為“Application”SourceName為“NET Runtime”MachineName為本地計算機的名稱。

這些欄位都可以透過EventLogSettings進行修改:

csharp

public static IHostBuilder CreateHostBuilder(string[] args) =>    Host.CreateDefaultBuilder(args)        .ConfigureLogging(logging =>        {            logging.AddEventLog(settings =>            {                settings.LogName = "My App";                settings.SourceName = "My Log";                settings.MachineName = "My Computer";            })        })        .ConfigureWebHostDefaults(webBuilder =>        {            webBuilder.UseStartup<Startup>();        });

日誌記錄過濾器

透過日誌記錄過濾器,允許你書寫複雜的邏輯,來控制是否要記錄日誌。

csharp

public static IHostBuilder CreateHostBuilder(string[] args) =>    Host.CreateDefaultBuilder(args)        .ConfigureLogging(logging =>        {            logging                // 針對所有 LoggerProvider 設定 Microsoft 最小日誌級別,建議透過配置檔案進行配置                .AddFilter("Microsoft", LogLevel.Trace)                // 針對 ConsoleLoggerProvider 設定 Microsoft 最小日誌級別,建議透過配置檔案進行配置                .AddFilter<ConsoleLoggerProvider>("Microsoft", LogLevel.Debug)                // 針對所有 LoggerProvider 進行過濾配置                .AddFilter((provider, category, logLevel) =>                {                    // 由於下面單獨針對 ConsoleLoggerProvider 添加了過濾配置,所以 ConsoleLoggerProvider 不會進入該方法                                    if (provider == typeof(ConsoleLoggerProvider).FullName                        && category == typeof(ValuesController).FullName                        && logLevel <= LogLevel.Warning)                    {                        // false:不記錄日誌                        return false;                    }                    // true:記錄日誌                    return true;                })                // 針對 ConsoleLoggerProvider 進行過濾配置                .AddFilter<ConsoleLoggerProvider>((category, logLevel) =>                {                    if (category == typeof(ValuesController).FullName                        && logLevel <= LogLevel.Warning)                    {                        // false:不記錄日誌                        return false;                    }                    // true:記錄日誌                    return true;                });        })        .ConfigureWebHostDefaults(webBuilder =>        {            webBuilder.UseStartup<Startup>();        });

日誌訊息模版

應用開發過程中,對於某一類的日誌,我們希望它們的訊息格式保持一致,僅僅是某些引數發生變化。這就要用到日誌訊息模板了。

舉個例子:

csharp

[HttpGet("{id}")]public int Get(int id){    _logger.LogInformation("Get {Id}", id);    return id;}

其中Get {Id}就是一個日誌訊息模板,{Id}則是模板引數(注意,請在裡面書寫名稱,而不是數字,這樣更容易理解引數含義)。

不過,需要注意的是,{Id}這個模板引數,僅僅是用於讓人容易理解其含義的,和後面的引數名沒有任何關係,模板值關心引數的順序。例如:

csharp

[HttpGet("{id}")]public int Get(int id){    _logger.LogInformation("Get {Id} at {Time}", DateTime.Now, id);    return id;}

假設傳入id = 1,它的輸出是:Get 11/02/2021 11:42:14 at 1

日誌訊息模板是一項非常重要的功能,在眾多開源日誌中介軟體中,均有使用。

主機構建期間的日誌記錄

ASP.NET Core框架不直接支援在主機構建期間進行日誌記錄。但是可以透過獨立的日誌記錄提供程式進行日誌記錄,例如,使用第三方日誌記錄提供程式:Serilog

安裝Nuget包:Install-Package Serilog.AspNetCore

csharp

public static void Main(string[] args){    // 從appsettings.json和命令列引數中讀取配置    var config = new ConfigurationBuilder()        .AddJsonFile("appsettings.json")        .AddCommandLine(args)        .Build();    // 建立Logger    Log.Logger = new LoggerConfiguration()        .WriteTo.Console()                          // 輸出到控制檯        .WriteTo.File(config["Logging:File:Path"])  // 輸出到指定檔案        .CreateLogger();    try    {        CreateHostBuilder(args).Build().Run();    }    catch(Exception ex)    {        Log.Fatal(ex, "Host terminated unexpectedly");        throw;    }    finally    {        Log.CloseAndFlush();    }}

appsettings.json

json

{  "Logging": {    "File": {      "Path": "logs/host.log"    }  }}

控制檯日誌格式配置

控制檯日誌記錄提供程式是我們開發過程中必不可少的,透過上面我們已經得知可以透過AddConsole()進行新增。不過它的侷限性比較大,日誌格式我們都無法進行自定義。

因此,在.NET 5中,對控制檯日誌記錄提供程式進行了擴充套件,預置了三種日誌輸出格式:Json、Simple、Systemd。

實際上,之前也有列舉ConsoleLoggerFormat提供了Simple和Systemd格式,不過不能進行自定義,已經棄用了。

這些 Formatter 均繼承自抽象類ConsoleFormatter,該抽象類建構函式接收一個“名字”引數,要求其實現類必須擁有名字。你可以透過靜態類ConsoleFormatterNames獲取到內建的三種格式的名字。

csharp

public abstract class ConsoleFormatter{    protected ConsoleFormatter(string name)    {        Name = name ?? throw new ArgumentNullException(nameof(name));    }    public string Name { get; }    public abstract void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider scopeProvider, TextWriter textWriter);}public static class ConsoleFormatterNames{    public const string Simple = "simple";    public const string Json = "json";    public const string Systemd = "systemd";}

你可以在使用AddConsole()時,配置ConsoleLoggerOptionsFormatterName屬性,以達到自定義格式的目的,其預設值為“simple”。不過,為了方便使用,.NET 框架已經把內建的三種格式幫我們封裝好了。

這些 Formatter 的選項類均繼承自選項類ConsoleFormatterOptions,該選項類包含以下三個屬性:

csharp

public class ConsoleFormatterOptions{    // 啟用作用域,預設 false    public bool IncludeScopes { get; set; }    // 設定時間戳的格式,顯示在日誌訊息開頭    // 預設為 null,不展示時間戳    public string TimestampFormat { get; set; }    // 是否將時間戳時區設定為 UTC,預設是false,即本地時區    public bool UseUtcTimestamp { get; set; }}

SimpleConsoleFormatter

透過擴充套件方法AddSimpleConsole()可以新增支援Simple格式的控制檯日誌記錄提供程式,預設行為與AddConsole()一致。

csharp

.ConfigureLogging(logging =>{    logging.ClearProviders()        .AddSimpleConsole();}

示例輸出:

stylus

info: Microsoft.Hosting.Lifetime[0]      Now listening on: http://localhost:5000info: Microsoft.Hosting.Lifetime[0]      Application started. Press Ctrl+C to shut down.info: Microsoft.Hosting.Lifetime[0]      Hosting environment: Developmentinfo: Microsoft.Hosting.Lifetime[0]      Content root path: C:\Repos\WebApplication

另外,你可以透過SimpleConsoleFormatterOptions進行一些自定義配置:

gradle

.ConfigureLogging(logging =>{    logging.ClearProviders()        .AddSimpleConsole(options =>         {            // 一條日誌訊息展示在同一行            options.SingleLine = true;            options.IncludeScopes = true;            options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";            options.UseUtcTimestamp = false;        });}

示例輸出:

apache

2021-11-02 15:53:33 info: Microsoft.Hosting.Lifetime[0] Now listening on: http://localhost:50002021-11-02 15:53:33 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down.2021-11-02 15:53:33 info: Microsoft.Hosting.Lifetime[0] Hosting environment: Development2021-11-02 15:53:33 info: Microsoft.Hosting.Lifetime[0] Content root path: C:\Repos\WebApplication

SystemdConsoleFormatter

透過擴充套件方法AddSystemdConsole()可以新增支援Systemd格式的控制檯日誌記錄提供程式。如果你熟悉Linux,那你對它也一定不陌生。

csharp

.ConfigureLogging(logging =>{    logging.ClearProviders()        .AddSystemdConsole();}

示例輸出:

stylus

<6>Microsoft.Hosting.Lifetime[0] Now listening on: http://localhost:5000<6>Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down.<6>Microsoft.Hosting.Lifetime[0] Hosting environment: Development<6>Microsoft.Hosting.Lifetime[0] Content root path: C:\Repos\WebApplication

前面的<6>表示日誌級別info,如果你有興趣瞭解Systemd,可以訪問阮一峰老師的Systemd 入門教程:命令篇

JsonConsoleFormatter

透過擴充套件方法AddJsonConsole()可以新增支援Json格式的控制檯日誌記錄提供程式。

csharp

.ConfigureLogging(logging =>{    logging.ClearProviders()        .AddJsonConsole(options =>        {            options.JsonWriterOptions = new JsonWriterOptions            {                // 啟用縮排,看起來更舒服                Indented = true            };        });}

示例輸出:

json

{  "EventId": 0,  "LogLevel": "Information",  "Category": "Microsoft.Hosting.Lifetime",  "Message": "Now listening on: http://localhost:5000",  "State": {    "Message": "Now listening on: http://localhost:5000",    "address": "http://localhost:5000",    "{OriginalFormat}": "Now listening on: {address}"  }}{  "EventId": 0,  "LogLevel": "Information",  "Category": "Microsoft.Hosting.Lifetime",  "Message": "Application started. Press Ctrl\u002BC to shut down.",  "State": {    "Message": "Application started. Press Ctrl\u002BC to shut down.",    "{OriginalFormat}": "Application started. Press Ctrl\u002BC to shut down."  }}{  "EventId": 0,  "LogLevel": "Information",  "Category": "Microsoft.Hosting.Lifetime",  "Message": "Hosting environment: Development",  "State": {    "Message": "Hosting environment: Development",    "envName": "Development",    "{OriginalFormat}": "Hosting environment: {envName}"  }}{  "EventId": 0,  "LogLevel": "Information",  "Category": "Microsoft.Hosting.Lifetime",  "Message": "Content root path: C:\\Repos\\WebApplication",  "State": {    "Message": "Content root path: C:\\Repos\\WebApplication",    "contentRoot": "C:\\Repos\\WebApplication",    "{OriginalFormat}": "Content root path: {contentRoot}"  }}

如果你同時添加了多種格式的控制檯記錄程式,那麼只有最後一個新增的生效。

以上介紹的是透過程式碼進行控制檯日誌記錄提供程式的設定,不過我想大家應該更喜歡透過配置去設定日誌記錄提供程式。下面是一個簡單地配置示例:

json

{  "Logging": {    "LogLevel": {      "Default": "Information",      "Microsoft": "Warning",      "Microsoft.Hosting.Lifetime": "Information"    },    "Console": {      "FormatterName": "json",      "FormatterOptions": {        "SingleLine": true,        "IncludeScopes": true,        "TimestampFormat": "yyyy-MM-dd HH:mm:ss ",        "UseUtcTimestamp": false,        "JsonWriterOptions": {          "Indented": true        }      }    }  }}

ILogger<TCategoryName>物件例項的建立

講到這裡,不知道你會不會對ILogger<TCategoryName>物件例項的建立有疑惑:它到底是如何被new出來的呢?

要解決這個問題,我們先從AddLogging()擴充套件方法入手:

csharp

public static class LoggingServiceCollectionExtensions{    public static IServiceCollection AddLogging(this IServiceCollection services)    {        return AddLogging(services, builder => { });    }    public static IServiceCollection AddLogging(this IServiceCollection services, Action<ILoggingBuilder> configure)    {        services.AddOptions();        // 註冊單例 ILoggerFactory        services.TryAdd(ServiceDescriptor.Singleton<ILoggerFactory, LoggerFactory>());        // 註冊單例 ILogger<>        services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>)));        // 批次註冊單例 IConfigureOptions<LoggerFilterOptions>        services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<LoggerFilterOptions>>(            new DefaultLoggerLevelConfigureOptions(LogLevel.Information)));        configure(new LoggingBuilder(services));        return services;    }}

你可能也猜到了,這個Logger<>不會是LoggerFactory建立的吧?要不然註冊個這玩意幹嘛呢?

彆著急,咱們接著先檢視ILogger<>服務的實現類Logger<>

csharp

public interface 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);}public interface ILogger<out TCategoryName> : ILogger{}    public class Logger<T> : ILogger<T>{    // 介面實現內部均是使用該例項進行操作    private readonly ILogger _logger;    // 果不其然,注入了 ILoggerFactory 例項    public Logger(ILoggerFactory factory)    {        // 還記得嗎?上面提到顯式指定日誌類別時,也是這樣建立 ILogger 例項的        _logger = factory.CreateLogger(TypeNameHelper.GetTypeDisplayName(typeof(T), includeGenericParameters: false, nestedTypeDelimiter: '.'));    }        // ...}

沒錯,你猜對了,那就來看看這個LoggerFactory吧(只列舉核心程式碼):

csharp

public interface ILoggerFactory : IDisposable{    ILogger CreateLogger(string categoryName);    void AddProvider(ILoggerProvider provider);}public class LoggerFactory : ILoggerFactory{    // 用於單例化 Logger<>    private readonly Dictionary<string, Logger> _loggers = new Dictionary<string, Logger>(StringComparer.Ordinal);    // 存放 ILoggerProviderRegistrations    private readonly List<ProviderRegistration> _providerRegistrations = new List<ProviderRegistration>();    private readonly object _sync = new object();    public LoggerFactory(IEnumerable<ILoggerProvider> providers, IOptionsMonitor<LoggerFilterOptions> filterOption, IOptions<LoggerFactoryOptions> options = null)    {        // ...        // 註冊 ILoggerProviders        foreach (ILoggerProvider provider in providers)        {            AddProviderRegistration(provider, dispose: false);        }        // ...    }    public ILogger CreateLogger(string categoryName)    {        lock (_sync)        {            // 如果不存在,則 new            if (!_loggers.TryGetValue(categoryName, out Logger logger))            {                logger = new Logger                {                    Loggers = CreateLoggers(categoryName),                };                (logger.MessageLoggers, logger.ScopeLoggers) = ApplyFilters(logger.Loggers);                // 單例化 Logger<>                _loggers[categoryName] = logger;            }            return logger;        }    }        private void AddProviderRegistration(ILoggerProvider provider, bool dispose)    {        _providerRegistrations.Add(new ProviderRegistration        {            Provider = provider,            ShouldDispose = dispose        });                // ...    }        private LoggerInformation[] CreateLoggers(string categoryName)    {        var loggers = new LoggerInformation[_providerRegistrations.Count];        // 迴圈遍歷所有 ILoggerProvider        for (int i = 0; i < _providerRegistrations.Count; i++)        {            loggers[i] = new LoggerInformation(_providerRegistrations[i].Provider, categoryName);        }        return loggers;    }}

注意若要在Startup.Configure方法中記錄日誌,直接在引數上注入ILogger<Startup>即可。不支援在Startup.ConfigureServices方法中使用ILogger,因為此時DI容器還未配置完成。沒有非同步的日誌記錄方法。日誌記錄動作執行應該很快,不值的犧牲效能使用非同步方法。如果日誌記錄動作比較耗時,如記錄到MSSQL中,那麼請不要直接寫入MSSQL。你應該考慮先將日誌寫入到快速儲存介質,如記憶體佇列,然後通過後臺工作執行緒將其從記憶體轉儲到MSSQL中。無法使用日誌記錄 API 在應用執行時更改日誌記錄配置。不過,一些配置提供程式(如檔案配置提供程式)可重新載入配置,這可以立即更新日誌記錄配置。

小結Host.CreateDefaultBuilder方法中,預設添加了ConsoleDebugEventSourceEventLog(僅Windows)共四種日誌記錄提供程式(Logger Provider)。透過注入服務ILogger<TCategoryName>,可以方便的進行日誌記錄。可以透過程式碼或配置對日誌記錄提供程式進行設定,如LogLevelFormatterName等。可以透過擴充套件方法AddFilter新增日誌記錄過濾器,允許你書寫複雜的邏輯,來控制是否要記錄日誌。支援日誌訊息模板。對於控制檯記錄日誌程式,.NET框架內建了Simple(預設)、SystemdJson三種日誌輸出格式。.NET 6 預覽版中新增了一個稱為“編譯時日誌記錄源生成”的功能,該功能非常實用,有興趣的可以先去了解一下。最後,給大家列舉一些常用的日誌開源中介軟體:SerilogLog4NetNLog

歡迎點贊+轉發+關注!大家的支援是我分享最大的動力!!!

5
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 配置DNS over HTTPS來阻止DNS汙染