Util应用框架基础(六) - 日志记录(一) - 正文

本文介绍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 配置文件设置日志级别.

json 复制代码
{
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  }
}

Logging 配置节用于配置日志.

LogLevel 为所有日志提供程序配置日志级别.

Default 为所有日志类别设置默认的日志级别.

上面的配置将默认日志级别设置为 Information.

意味着只输出日志级别等于或大于 Information 的日志.

现在 Trace 和 Debug 两个级别的日志被禁用了.

可以为特定日志类别 设置日志级别.

json 复制代码
{
  "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 支持泛型参数, 用来指定日志类别,使用带命名空间的类名作为日志类别.

c# 复制代码
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 完成上面相同的示例.

c# 复制代码
public class DemoController : WebApiControllerBase {
    public DemoController( ILog<DemoController> log ) {
        log.Message( "Util" ).LogDebug();
    }
}

Message 是 ILog 定义的方法, 用来设置日志消息,可以多次调用它拼接内容.

当你需要写比较长的日志内容, ILog 可以帮你拼接内容,这样省去了定义 StringBuilder 的麻烦.

可以在 ILog 添加自定义扩展方法来设置内容, Util应用框架内置了一些设置日志消息的扩展方法, 比如 AppendLine.

c# 复制代码
public class DemoController : WebApiControllerBase {
    public DemoController( ILog<DemoController> log ) {
        log.AppendLine( "内容1" )
            .AppendLine( "内容2" )
            .LogDebug();
    }
}

你可以定义自己的扩展方法,以更加语义化的方式记录日志.

范例:

c# 复制代码
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 相关方法.

c# 复制代码
/// <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应用框架内置了几个日志操作扩展方法,你可以定义自己的扩展方法,以方便内容设置.

c# 复制代码
/// <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 日志上下文

通过日志上下文自动记录重要的额外信息.

c# 复制代码
/// <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 .

c# 复制代码
/// <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 获取日志上下文.

c# 复制代码
/// <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 环境获取依赖服务是安全的,但在其它环境则可能获取失败.

如果获取日志上下文失败,也不会对功能造成影响,只是丢失了一些上下文信息.

c# 复制代码
/// <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 上.

c# 复制代码
/// <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 日志级别.

  • 设置日志上下文扩展.

c# 复制代码
/// <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 日志级别.

  • 设置日志上下文扩展.

c# 复制代码
/// <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";
        }
    }
}