Util應用框架基礎(六) - 日誌記錄(一) - 正文

何鎮汐發表於2023-11-11

本文介紹Util應用框架如何記錄日誌.

日誌記錄共分4篇,本文是正文,後續還有3篇分別介紹寫入不同日誌接收器的安裝和配置方法.

概述

日誌記錄對於瞭解系統執行情況非常重要.

Asp.Net Core 抽象了日誌基礎架構,支援使用日誌提供程式進行擴充套件,提供控制檯日誌等簡單實現.

Serilog 是 .Net 流行的第三方日誌框架,支援結構化日誌,並能與 Asp.Net Core 日誌整合.

Serilog 支援多種日誌接收器,可以將日誌傳送到不同的地方.

我們可以將日誌寫入文字檔案,但檢視文字檔案比較困難,檔案如果很大,查詢問題非常費力.

對於生產環境,我們需要包含管理介面的日誌系統.

Seq 是一個日誌系統,可以很好的展示結構化日誌資料,並提供模糊搜尋功能.

Exceptionless 是基於 Asp.Net Core 開發的日誌系統.

與 Seq 相比,Exceptionless 搜尋能力較弱.

Seq 和 Exceptionless 都提供了 Serilog 日誌接收器,可以使用 Serilog 接入它們.

Util應用框架使用 Serilog 日誌框架,同時整合了 SeqExceptionless 日誌系統.

Util簡化了日誌配置,並對常用功能進行擴充套件.

日誌配置

選擇日誌接收器

Util應用框架預設支援三種 Serilog 日誌接收器:

  • 日誌檔案
  • Seq
  • Exceptionless

你可以從中選擇一種或多種,如果都不能滿足要求,你也可以引用 Serilog 支援的其它日誌接收器,或自行實現.

配置日誌接收器

請轉到特定日誌接收器章節檢視配置方法.

配置日誌級別

Asp.Net Core 使用日誌級別表示日誌的嚴重程度,定義如下:

  • Trace = 0
  • Debug = 1
  • Information = 2
  • Warning = 3
  • Error = 4
  • Critical = 5
  • None = 6

None不開啟日誌,Trace的嚴重程度最低,Critical的嚴重程度最高,需要高度關注.

可以在 appsettings.json 配置檔案設定日誌級別.

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

Logging 配置節用於配置日誌.

LogLevel 為所有日誌提供程式配置日誌級別.

Default 為所有日誌類別設定預設的日誌級別.

上面的配置將預設日誌級別設定為 Information.

意味著只輸出日誌級別等於或大於 Information 的日誌.

現在 Trace 和 Debug 兩個級別的日誌被禁用了.

可以為特定日誌類別設定日誌級別.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
    }
  }
}

配置增加了 Microsoft 日誌類別,並設定為 Debug 日誌級別.

日誌類別用來給日誌分類,一般使用帶名稱空間的類名作為日誌類別.

日誌類別支援模糊匹配, Microsoft 日誌類別不僅匹配 Microsoft ,而且還能匹配以 Microsoft 開頭的所有日誌類別,比如 Microsoft.AspNetCore .

Serilog 的日誌級別

Serilog 定義了自己的日誌級別,不支援上面介紹的標準配置方式.

Exceptionless 也是如此.

使用第三方框架的日誌級別會導致複雜性.

Util應用框架擴充套件了 Serilog 和 Exceptionless 的日誌級別配置,允許以統一的標準方式進行配置.

記錄日誌

.Net 提供了標準的日誌記錄介面 Microsoft.Extensions.Logging.ILogger.

你可以使用 ILogger 記錄日誌.

Util應用框架還提供了一個 Util.Logging.ILog 介面.

當你需要寫入很長的日誌時,可能需要使用 StringBuilder 拼接日誌內容.

ILog 提供了一種更簡單的方式寫入長內容日誌.

使用 ILogger 記錄日誌

ILogger 支援泛型引數, 用來指定日誌類別,使用帶名稱空間的類名作為日誌類別.

namespace Demo;

public class DemoController : WebApiControllerBase {
    public DemoController( ILogger<DemoController> logger ) {
        logger.LogDebug( "Util" );
    }
}

示例在控制器構造方法注入 ILogger<DemoController> ,日誌類別為 Demo.DemoController .

ILogger 擴充套件了一些以 Log 開頭的方法,比如 LogDebug,表示寫入日誌級別為 Debug 的訊息.

logger.LogDebug( "Util" ) 以 Debug 日誌級別寫入訊息'Util'.

使用 ILog 記錄日誌

Util應用框架定義了 ILog 介面.

ILog 是對 ILogger 介面的簡單包裝, 對日誌內容的設定進行了擴充套件.

ILog 也使用泛型引數來指定日誌類別.

使用 ILog 完成上面相同的示例.

public class DemoController : WebApiControllerBase {
    public DemoController( ILog<DemoController> log ) {
        log.Message( "Util" ).LogDebug();
    }
}

Message 是 ILog 定義的方法, 用來設定日誌訊息,可以多次呼叫它拼接內容.

當你需要寫比較長的日誌內容, ILog 可以幫你拼接內容,這樣省去了定義 StringBuilder 的麻煩.

可以在 ILog 新增自定義擴充套件方法來設定內容, Util應用框架內建了一些設定日誌訊息的擴充套件方法, 比如 AppendLine.

public class DemoController : WebApiControllerBase {
    public DemoController( ILog<DemoController> log ) {
        log.AppendLine( "內容1" )
            .AppendLine( "內容2" )
            .LogDebug();
    }
}

你可以定義自己的擴充套件方法,以更加語義化的方式記錄日誌.

範例:

public class DemoController : WebApiControllerBase {
    public DemoController( ILog<DemoController> log ) {
        log.Caption( "標題" )
            .Content( "內容" )
            .Sql( "Sql" )
            .LogDebug();
    }
}

ILog 與 ILogger 比較:

ILog 更擅長記錄內容很長的日誌.

ILog 是有狀態服務,不能在多個執行緒共享使用.

可以使用 ILog 記錄業務日誌,其它場景應使用 ILogger.

結構化日誌支援

Serilog 日誌框架對結構化日誌提供了支援.

結構化日誌使用特定語法的訊息模板日誌格式,可以從日誌文字中提取搜尋元素.

結構化日誌的優勢主要體現在日誌系統對日誌訊息的展示和搜尋方式上.

不同的日誌系統對結構化日誌的展示方式和搜尋能力不同.

請參考 Seq 和 Exceptionless 的 結構化日誌支援 小節.

日誌操作上下文

記錄日誌時,我們除了需要記錄業務內容,還需要知道一些額外的資訊,比如操作使用者是誰.

我們希望記錄日誌時僅設定業務內容,這些額外的資訊最好能自動記錄.

Util應用框架透過日誌上下文自動設定這些額外資訊.

  • UserId 設定當前操作使用者標識

  • Application 設定當前應用程式名稱.

  • Environment 設定當前環境名稱.

  • TraceId 設定跟蹤號.

  • Stopwatch 設定計時器,用於記錄請求執行花了多長時間.

在 Asp.Net Core 環境, 日誌上下文由日誌上下文中介軟體 Util.Applications.Logging.LogContextMiddleware 建立.

無需手工新增日誌上下文中介軟體,只要引用 Util.Application.WebApi 類庫, 就會自動新增到中介軟體管道.

對於 Web 請求, 跟蹤號是一個重要的資訊,可以透過查詢跟蹤號,將相關的請求日誌全部查出來.

另外, Exceptionless 會自動收集很多系統資訊.

原始碼解析

ILog 日誌操作

ILog 日誌操作介面提供鏈式呼叫方式設定日誌內容.

  • Message 方法設定日誌訊息.

  • Property 方法設定擴充套件屬性.

  • State 設定日誌引數物件.

Log 開頭的日誌記錄方法,將日誌操作委託給 ILogger 相關方法.

/// <summary>
/// 日誌操作
/// </summary>
/// <typeparam name="TCategoryName">日誌類別</typeparam>
public interface ILog<out TCategoryName> : ILog {
}

/// <summary>
/// 日誌操作
/// </summary>
public interface ILog {
    /// <summary>
    /// 設定日誌事件標識
    /// </summary>
    /// <param name="eventId">日誌事件標識</param>
    ILog EventId( EventId eventId );
    /// <summary>
    /// 設定異常
    /// </summary>
    /// <param name="exception">異常</param>
    ILog Exception( Exception exception );
    /// <summary>
    /// 設定自定義擴充套件屬性
    /// </summary>
    /// <param name="propertyName">屬性名</param>
    /// <param name="propertyValue">屬性值</param>
    ILog Property( string propertyName, string propertyValue );
    /// <summary>
    /// 設定日誌狀態物件
    /// </summary>
    /// <param name="state">狀態物件</param>
    ILog State( object state );
    /// <summary>
    /// 設定日誌訊息
    /// </summary>
    /// <param name="message">日誌訊息</param>
    /// <param name="args">日誌訊息引數</param>
    ILog Message( string message, params object[] args );
    /// <summary>
    /// 是否啟用
    /// </summary>
    /// <param name="logLevel">日誌級別</param>
    bool IsEnabled( LogLevel logLevel );
    /// <summary>
    /// 開啟日誌範圍
    /// </summary>
    /// <typeparam name="TState">日誌狀態型別</typeparam>
    /// <param name="state">日誌狀態</param>
    IDisposable BeginScope<TState>( TState state );
    /// <summary>
    /// 寫跟蹤日誌
    /// </summary>
    ILog LogTrace();
    /// <summary>
    /// 寫除錯日誌
    /// </summary>
    ILog LogDebug();
    /// <summary>
    /// 寫資訊日誌
    /// </summary>
    ILog LogInformation();
    /// <summary>
    /// 寫警告日誌
    /// </summary>
    ILog LogWarning();
    /// <summary>
    /// 寫錯誤日誌
    /// </summary>
    ILog LogError();
    /// <summary>
    /// 寫致命日誌
    /// </summary>
    ILog LogCritical();
}

ILogExtensions 日誌操作擴充套件

Util應用框架內建了幾個日誌操作擴充套件方法,你可以定義自己的擴充套件方法,以方便內容設定.

/// <summary>
/// 日誌操作擴充套件
/// </summary>
public static class ILogExtensions {
    /// <summary>
    /// 新增訊息
    /// </summary>
    /// <param name="log">配置項</param>
    /// <param name="message">訊息</param>
    /// <param name="args">日誌訊息引數</param>
    public static ILog Append( this ILog log,string message, params object[] args ) {
        log.CheckNull( nameof( log ) );
        log.Message( message, args );
        return log;
    }

    /// <summary>
    /// 當條件為true新增訊息
    /// </summary>
    /// <param name="log">配置項</param>
    /// <param name="message">訊息</param>
    /// <param name="condition">條件,值為true則新增訊息</param>
    /// <param name="args">日誌訊息引數</param>
    public static ILog AppendIf( this ILog log, string message,bool condition, params object[] args ) {
        log.CheckNull( nameof( log ) );
        if ( condition )
            log.Message( message, args );
        return log;
    }

    /// <summary>
    /// 新增訊息並換行
    /// </summary>
    /// <param name="log">配置項</param>
    /// <param name="message">訊息</param>
    /// <param name="args">日誌訊息引數</param>
    public static ILog AppendLine( this ILog log, string message, params object[] args ) {
        log.CheckNull( nameof( log ) );
        log.Message( message, args );
        log.Message( Environment.NewLine );
        return log;
    }

    /// <summary>
    /// 當條件為true新增訊息並換行
    /// </summary>
    /// <param name="log">配置項</param>
    /// <param name="message">訊息</param>
    /// <param name="condition">條件,值為true則新增訊息</param>
    /// <param name="args">日誌訊息引數</param>
    public static ILog AppendLineIf( this ILog log, string message, bool condition, params object[] args ) {
        log.CheckNull( nameof( log ) );
        if ( condition ) {
            log.Message( message, args );
            log.Message( Environment.NewLine );
        }
        return log;
    }

    /// <summary>
    /// 訊息換行
    /// </summary>
    /// <param name="log">配置項</param>
    public static ILog Line( this ILog log ) {
        log.CheckNull( nameof(log) );
        log.Message( Environment.NewLine );
        return log;
    }
}

LogContext 日誌上下文

透過日誌上下文自動記錄重要的額外資訊.

/// <summary>
/// 日誌上下文
/// </summary>
public class LogContext {
    /// <summary>
    /// 初始化日誌上下文
    /// </summary>
    public LogContext() {
        Data = new Dictionary<string, object>();
    }

    /// <summary>
    /// 計時器
    /// </summary>
    public Stopwatch Stopwatch { get; set; }
    /// <summary>
    /// 跟蹤標識
    /// </summary>
    public string TraceId { get; set; }
    /// <summary>
    /// 使用者標識
    /// </summary>
    public string UserId { get; set; }
    /// <summary>
    /// 應用程式
    /// </summary>
    public string Application { get; set; }
    /// <summary>
    /// 執行環境
    /// </summary>
    public string Environment { get; set; }
    /// <summary>
    /// 擴充套件資料
    /// </summary>
    public IDictionary<string, object> Data { get; }
}

LogContextMiddleware 日誌上下文中介軟體

日誌上下文中介軟體建立日誌上下文,並新增到 HttpContext 物件的 Items .

/// <summary>
/// 日誌上下文中介軟體
/// </summary>
public class LogContextMiddleware {
    /// <summary>
    /// 下箇中介軟體
    /// </summary>
    private readonly RequestDelegate _next;

    /// <summary>
    /// 初始化日誌上下文中介軟體
    /// </summary>
    /// <param name="next">下箇中介軟體</param>
    public LogContextMiddleware( RequestDelegate next ) {
        _next = next;
    }

    /// <summary>
    /// 執行中介軟體
    /// </summary>
    /// <param name="context">Http上下文</param>
    public async Task Invoke( HttpContext context ) {
        var traceId = context.Request.Headers["x-correlation-id"].SafeString();
        if ( traceId.IsEmpty() )
            traceId = context.TraceIdentifier;
        var session = context.RequestServices.GetService<Util.Sessions.ISession>();
        var environment = context.RequestServices.GetService<IWebHostEnvironment>();
        var logContext = new LogContext {
            Stopwatch = Stopwatch.StartNew(), 
            TraceId = traceId, 
            UserId = session?.UserId,
            Application = environment?.ApplicationName,
            Environment = environment?.EnvironmentName
        };
        context.Items[LogContextAccessor.LogContextKey] = logContext;
        await _next( context );
    }
}

ILogContextAccessor 日誌上下文訪問器

日誌上下文訪問器從 HttpContext.Items 獲取日誌上下文.

/// <summary>
/// 日誌上下文訪問器
/// </summary>
public interface ILogContextAccessor {
    /// <summary>
    /// 日誌上下文
    /// </summary>
    LogContext Context { get; set; }
}

/// <summary>
/// 日誌上下文訪問器
/// </summary>
public class LogContextAccessor : ILogContextAccessor {
    /// <summary>
    /// 日誌上下文鍵名
    /// </summary>
    public const string LogContextKey = "Util.Logging.LogContext";

    /// <summary>
    /// 日誌上下文
    /// </summary>
    public LogContext Context {
        get => Util.Helpers.Convert.To<LogContext>( Web.HttpContext.Items[LogContextKey] );
        set => Web.HttpContext.Items[LogContextKey] = value;
    }
}

LogContextEnricher 日誌上下文擴充套件

Serilog 提供 ILogEventEnricher 介面用於設定擴充套件屬性.

LogContextEnricher 使用 Ioc.Create 方法獲取依賴服務 ILogContextAccessor.

這是因為不能使用依賴注入,它要求實現類必須是無參建構函式.

Ioc.Create 在 Asp.Net Core 環境獲取依賴服務是安全的,但在其它環境則可能獲取失敗.

如果獲取日誌上下文失敗,也不會對功能造成影響,只是丟失了一些上下文資訊.

/// <summary>
/// 日誌上下文擴充套件屬性
/// </summary>
public class LogContextEnricher : ILogEventEnricher {
    /// <summary>
    /// 擴充套件屬性
    /// </summary>
    /// <param name="logEvent">日誌事件</param>
    /// <param name="propertyFactory">日誌事件屬性工廠</param>
    public void Enrich( LogEvent logEvent, ILogEventPropertyFactory propertyFactory ) {
        var accessor = Ioc.Create<ILogContextAccessor>();
        if ( accessor == null )
            return;
        var context = accessor.Context;
        if ( context == null )
            return;
        if ( logEvent == null )
            return;
        if ( propertyFactory == null )
            return;
        RemoveProperties( logEvent );
        AddDuration( context,logEvent, propertyFactory );
        AddTraceId( context, logEvent, propertyFactory );
        AddUserId( context, logEvent, propertyFactory );
        AddApplication( context, logEvent, propertyFactory );
        AddEnvironment( context, logEvent, propertyFactory );
        AddData( context, logEvent, propertyFactory );
    }

    /// <summary>
    /// 移除預設設定的部分屬性
    /// </summary>
    private void RemoveProperties( LogEvent logEvent ) {
        logEvent.RemovePropertyIfPresent( "ActionId" );
        logEvent.RemovePropertyIfPresent( "ActionName" );
        logEvent.RemovePropertyIfPresent( "RequestId" );
        logEvent.RemovePropertyIfPresent( "RequestPath" );
        logEvent.RemovePropertyIfPresent( "ConnectionId" );
    }

    /// <summary>
    /// 新增執行持續時間
    /// </summary>
    private void AddDuration( LogContext context, LogEvent logEvent, ILogEventPropertyFactory propertyFactory ) {
        if ( context?.Stopwatch == null )
            return;
        var property = propertyFactory.CreateProperty( "Duration", context.Stopwatch.Elapsed.Description() );
        logEvent.AddOrUpdateProperty( property );
    }

    /// <summary>
    /// 新增跟蹤號
    /// </summary>
    private void AddTraceId( LogContext context, LogEvent logEvent, ILogEventPropertyFactory propertyFactory ) {
        if ( context == null || context.TraceId.IsEmpty() )
            return;
        var property = propertyFactory.CreateProperty( "TraceId", context.TraceId );
        logEvent.AddOrUpdateProperty( property );
    }

    /// <summary>
    /// 新增使用者標識
    /// </summary>
    private void AddUserId( LogContext context, LogEvent logEvent, ILogEventPropertyFactory propertyFactory ) {
        if ( context == null || context.UserId.IsEmpty() )
            return;
        var property = propertyFactory.CreateProperty( "UserId", context.UserId );
        logEvent.AddOrUpdateProperty( property );
    }

    /// <summary>
    /// 新增應用程式
    /// </summary>
    private void AddApplication( LogContext context, LogEvent logEvent, ILogEventPropertyFactory propertyFactory ) {
        if ( context == null || context.Application.IsEmpty() )
            return;
        var property = propertyFactory.CreateProperty( "Application", context.Application );
        logEvent.AddOrUpdateProperty( property );
    }

    /// <summary>
    /// 新增執行環境
    /// </summary>
    private void AddEnvironment( LogContext context, LogEvent logEvent, ILogEventPropertyFactory propertyFactory ) {
        if ( context == null || context.Environment.IsEmpty() )
            return;
        var property = propertyFactory.CreateProperty( "Environment", context.Environment );
        logEvent.AddOrUpdateProperty( property );
    }

    /// <summary>
    /// 新增擴充套件資料
    /// </summary>
    private void AddData( LogContext context, LogEvent logEvent, ILogEventPropertyFactory propertyFactory ) {
        if ( context?.Data == null || context.Data.Count == 0 )
            return;
        foreach ( var item in context.Data ) {
            var property = propertyFactory.CreateProperty( item.Key, item.Value );
            logEvent.AddOrUpdateProperty( property );
        }
    }
}

LoggerEnrichmentConfigurationExtensions

將 LogContextEnricher 擴充套件到 LoggerEnrichmentConfiguration 上.

/// <summary>
/// Serilog擴充套件屬性配置擴充套件
/// </summary>
public static class LoggerEnrichmentConfigurationExtensions {
    /// <summary>
    /// 新增日誌上下文擴充套件屬性
    /// </summary>
    /// <param name="source">日誌擴充套件配置</param>
    public static LoggerConfiguration WithLogContext( this LoggerEnrichmentConfiguration source ) {
        source.CheckNull( nameof( source ) );
        return source.With<LogContextEnricher>();
    }

    /// <summary>
    /// 新增日誌級別擴充套件屬性
    /// </summary>
    /// <param name="source">日誌擴充套件配置</param>
    public static LoggerConfiguration WithLogLevel( this LoggerEnrichmentConfiguration source ) {
        source.CheckNull( nameof( source ) );
        return source.With<LogLevelEnricher>();
    }
}

AddSerilog 配置方法

AddSerilog 配置方法封裝了 Serilog 的配置.

  • 配置 ILog 介面服務依賴.

  • 將 Asp.Net Core 日誌級別轉換為 Serilog 日誌級別.

  • 設定日誌上下文擴充套件.

/// <summary>
/// Serilog日誌操作擴充套件
/// </summary>
public static class AppBuilderExtensions {
    /// <summary>
    /// 配置Serilog日誌操作
    /// </summary>
    /// <param name="builder">應用生成器</param>
    public static IAppBuilder AddSerilog( this IAppBuilder builder ) {
        return builder.AddSerilog( false );
    }

    /// <summary>
    /// 配置Serilog日誌操作
    /// </summary>
    /// <param name="builder">應用生成器</param>
    /// <param name="isClearProviders">是否清除預設設定的日誌提供程式</param>
    public static IAppBuilder AddSerilog( this IAppBuilder builder, bool isClearProviders ) {
        return builder.AddSerilog( options => {
            options.IsClearProviders = isClearProviders;
        } );
    }

    /// <summary>
    /// 配置Serilog日誌操作
    /// </summary>
    /// <param name="builder">應用生成器</param>
    /// <param name="appName">應用程式名稱</param>
    public static IAppBuilder AddSerilog( this IAppBuilder builder, string appName ) {
        return builder.AddSerilog( options => {
            options.Application = appName;
        } );
    }

    /// <summary>
    /// 配置Serilog日誌操作
    /// </summary>
    /// <param name="builder">應用生成器</param>
    /// <param name="setupAction">日誌配置操作</param>
    public static IAppBuilder AddSerilog( this IAppBuilder builder, Action<LogOptions> setupAction ) {
        builder.CheckNull( nameof( builder ) );
        var options = new LogOptions();
        setupAction?.Invoke( options );
        builder.Host.ConfigureServices( ( context, services ) => {
            services.AddSingleton<ILogFactory, LogFactory>();
            services.AddTransient( typeof( ILog<> ), typeof( Log<> ) );
            services.AddTransient( typeof( ILog ), t => t.GetService<ILogFactory>()?.CreateLog( "default" ) ?? NullLog.Instance );
            var configuration = context.Configuration;
            services.AddLogging( loggingBuilder => {
                if ( options.IsClearProviders )
                    loggingBuilder.ClearProviders();
                SerilogLog.Logger = new LoggerConfiguration()
                    .Enrich.WithProperty( "Application", options.Application )
                    .Enrich.FromLogContext()
                    .Enrich.WithLogLevel()
                    .Enrich.WithLogContext()
                    .ReadFrom.Configuration( configuration )
                    .ConfigLogLevel( configuration )
                    .CreateLogger();
                loggingBuilder.AddSerilog();
            } );
        } );
        return builder;
    }
}

AddExceptionless 配置方法

AddExceptionless 配置方法封裝了 Serilog 和 Exceptionless 的配置.

  • 配置 ILog 介面服務依賴.

  • 將 Asp.Net Core 日誌級別轉換為 Exceptionless 日誌級別.

  • 設定日誌上下文擴充套件.

/// <summary>
/// Exceptionless日誌操作擴充套件
/// </summary>
public static class AppBuilderExtensions {
    /// <summary>
    /// 配置Exceptionless日誌操作
    /// </summary>
    /// <param name="builder">應用生成器</param>
    /// <param name="isClearProviders">是否清除預設設定的日誌提供程式</param>
    public static IAppBuilder AddExceptionless( this IAppBuilder builder, bool isClearProviders = false ) {
        return builder.AddExceptionless( null, isClearProviders );
    }

    /// <summary>
    /// 配置Exceptionless日誌操作
    /// </summary>
    /// <param name="builder">應用生成器</param>
    /// <param name="appName">應用程式名稱</param>
    public static IAppBuilder AddExceptionless( this IAppBuilder builder, string appName ) {
        return builder.AddExceptionless( null, appName );
    }

    /// <summary>
    /// 配置Exceptionless日誌操作
    /// </summary>
    /// <param name="builder">應用生成器</param>
    /// <param name="configAction">Exceptionless日誌配置操作</param>
    /// <param name="isClearProviders">是否清除預設設定的日誌提供程式</param>
    public static IAppBuilder AddExceptionless( this IAppBuilder builder, Action<ExceptionlessConfiguration> configAction, bool isClearProviders = false ) {
        return builder.AddExceptionless( configAction, t => t.IsClearProviders = isClearProviders );
    }

    /// <summary>
    /// 配置Exceptionless日誌操作
    /// </summary>
    /// <param name="builder">應用生成器</param>
    /// <param name="configAction">Exceptionless日誌配置操作</param>
    /// <param name="appName">應用程式名稱</param>
    public static IAppBuilder AddExceptionless( this IAppBuilder builder, Action<ExceptionlessConfiguration> configAction, string appName ) {
        return builder.AddExceptionless( configAction, t => t.Application = appName );
    }

    /// <summary>
    /// 配置Exceptionless日誌操作
    /// </summary>
    /// <param name="builder">應用生成器</param>
    /// <param name="configAction">Exceptionless日誌配置操作</param>
    /// <param name="setupAction">日誌配置</param>
    public static IAppBuilder AddExceptionless( this IAppBuilder builder, Action<ExceptionlessConfiguration> configAction, Action<LogOptions> setupAction ) {
        builder.CheckNull( nameof( builder ) );
        var options = new LogOptions();
        setupAction?.Invoke( options );
        builder.Host.ConfigureServices( ( context, services ) => {
            services.AddSingleton<ILogFactory, LogFactory>();
            services.AddTransient( typeof( ILog<> ), typeof( Log<> ) );
            services.AddTransient( typeof( ILog ), t => t.GetService<ILogFactory>()?.CreateLog( "default" ) ?? NullLog.Instance );
            var configuration = context.Configuration;
            services.AddLogging( loggingBuilder => {
                if ( options.IsClearProviders )
                    loggingBuilder.ClearProviders();
                ConfigExceptionless( configAction, configuration );
                SerilogLog.Logger = new LoggerConfiguration()
                    .Enrich.WithProperty( "Application", options.Application )
                    .Enrich.FromLogContext()
                    .Enrich.WithLogLevel()
                    .Enrich.WithLogContext()
                    .WriteTo.Exceptionless()
                    .ReadFrom.Configuration( configuration )
                    .ConfigLogLevel( configuration )
                    .CreateLogger();
                loggingBuilder.AddSerilog();
            } );
        } );
        return builder;
    }

    /// <summary>
    /// 配置Exceptionless
    /// </summary>
    private static void ConfigExceptionless( Action<ExceptionlessConfiguration> configAction, IConfiguration configuration ) {
        ExceptionlessClient.Default.Startup();
        if ( configAction != null ) {
            configAction( ExceptionlessClient.Default.Configuration );
            ConfigLogLevel( configuration, ExceptionlessClient.Default.Configuration );
            return;
        }
        ExceptionlessClient.Default.Configuration.ReadFromConfiguration( configuration );
        ConfigLogLevel( configuration, ExceptionlessClient.Default.Configuration );
    }

    /// <summary>
    /// 配置日誌級別
    /// </summary>
    private static void ConfigLogLevel( IConfiguration configuration, ExceptionlessConfiguration options ) {
        var section = configuration.GetSection( "Logging:LogLevel" );
        foreach ( var item in section.GetChildren() ) {
            if ( item.Key == "Default" ) {
                options.Settings.Add( "@@log:*", GetLogLevel( item.Value ) );
                continue;
            }
            options.Settings.Add( $"@@log:{item.Key}*", GetLogLevel( item.Value ) );
        }
    }

    /// <summary>
    /// 獲取日誌級別
    /// </summary>
    private static string GetLogLevel( string logLevel ) {
        switch ( logLevel.ToUpperInvariant() ) {
            case "TRACE":
                return "Trace";
            case "DEBUG":
                return "Debug";
            case "INFORMATION":
                return "Info";
            case "ERROR":
                return "Error";
            case "CRITICAL":
                return "Fatal";
            case "NONE":
                return "Off";
            default:
                return "Warn";
        }
    }
}

相關文章