乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 浅析ASP.NET Core日志框架,通过Serilog来记录结构化日志

ASP.NET Core日志框架

ASP.NET Core提供了独立的日志模型,采用统一的API来完成日志的记录,支持各种内置日志记录器(如:Console、Debug、EventSource、EventLog、TraceSource等)和第三方日志框架(如:Log4Net、NLog、Loggr、Serilog、Sentry等),同时基于日志模型的扩展性,也可自定义更多的日志记录器。

ASP.NET Core日志框架的核心组件包是如下几个:

  • Microsoft.Extensions.Logging
  • Microsoft.Extensions.Logging.Console
  • Microsoft.Extensions.Logging.Debug
  • Microsoft.Extensions.Logging.TraceSource

image

社区第三方日志框架

image

社区中实现了对Microsoft.Extensions.Logging接口适配的第三方框架有

  • Sentry - provider for the Sentry service
  • Serilog - provider for the Serilog library
  • elmah.io - provider for the elmah.io service
  • Loggr - provider for the Loggr service
  • NLog - provider for the NLog library
  • Graylog - provider for the Graylog service
  • Sharpbrake - provider for the Airbrake notifier
  • KissLog.net - provider for the KissLog.net service

实践理解

https://github.com/TaylorShi/HelloLogging

准备示例项目

dotnet new sln -o HelloLogging
dotnet new console -o demoForConsole31 -f netcoreapp3.1
dotnet sln add .\demoForConsole31\demoForConsole31.csproj
explorer.exe .

image

准备配置文件

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    },
    "Console": {
      "LogLevel": {
        "Default": "Information",
        "Program": "Trace",
        "carLogger": "Trace"
      }
    }
  }
}

上诉配置文件中,Key代表了Logger的名称,Value代表了Logger的级别。

下面部分只针对Console的日志级别设定。

日志级别

其中日志级别的设定枚举定义为:

public enum LogLevel
{
    //
    // 摘要:
    //     Logs that contain the most detailed messages. These messages may contain sensitive
    //     application data. These messages are disabled by default and should never be
    //     enabled in a production environment.
    Trace,
    //
    // 摘要:
    //     Logs that are used for interactive investigation during development. These logs
    //     should primarily contain information useful for debugging and have no long-term
    //     value.
    Debug,
    //
    // 摘要:
    //     Logs that track the general flow of the application. These logs should have long-term
    //     value.
    Information,
    //
    // 摘要:
    //     Logs that highlight an abnormal or unexpected event in the application flow,
    //     but do not otherwise cause the application execution to stop.
    Warning,
    //
    // 摘要:
    //     Logs that highlight when the current flow of execution is stopped due to a failure.
    //     These should indicate a failure in the current activity, not an application-wide
    //     failure.
    Error,
    //
    // 摘要:
    //     Logs that describe an unrecoverable application or system crash, or a catastrophic
    //     failure that requires immediate attention.
    Critical,
    //
    // 摘要:
    //     Not used for writing log messages. Specifies that a logging category should not
    //     write any messages.
    None
}
日志级别 常用场景
Trace 记录一些对程序员调试问题有帮助的信息,其中可能包含一些敏感信息,所以应该避免在生产环境中启用Trace日志
Debug 记录一些在开发和调试阶段有用的短时变量(Short-termu sefulness),所以除非为了临时排除生产环境的故障,开发人员应该尽量避免在生产环境中启用Debug日志
Information 记录应用程序的一些流程,例如,记录当前api请求的url
Warning 记录应用程序中发生的不正常或者未预期的事件信息。这些信息中可能包含错误消息或者错误产生的条件,例如,文件未找到
Error 记录应用程序中某个操作产生的错误和异常信息。
Critical 记录一些需要立刻修复的问题。例如数据丢失,磁盘空间不足。

日志级别对应方法

查看ILogger的定义

void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter);
  • LogTrace
  • LogDebug
  • LogInformation
  • LogWarning
  • LogError
  • LogCritical

添加日志框架和配置绑定

依赖包

dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Logging
dotnet add package Microsoft.Extensions.Logging.Console
dotnet add package Microsoft.Extensions.Logging.Debug
dotnet add package Microsoft.Extensions.Logging.TraceSource

image

static void Main(string[] args)
{
    IConfigurationBuilder builder = new ConfigurationBuilder();
    builder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
    var config = builder.Build();

    IServiceCollection services = new ServiceCollection();
    // 使用工厂模式将配置对象注册到容器管理
    services.AddSingleton<IConfiguration>(p => config);

    services.AddLogging(builder =>
    {
        builder.AddConfiguration(config.GetSection("Logging"));
        builder.AddConsole();
    });

    IServiceProvider serviceProvider = services.BuildServiceProvider();
    ILoggerFactory loggerFactory = serviceProvider.GetService<ILoggerFactory>();
    ILogger logger = loggerFactory.CreateLogger("carLogger");
    logger.LogDebug("Tesla");
    logger.LogDebug(3011, "Tesla");
    logger.LogInformation("Hello Tesla");
    logger.LogWarning("Hi Warning");
    logger.LogError("Has Error");
    logger.LogError(new Exception("Hello Car"), "Has Error");

    Console.ReadKey();
}

这里我们看下AddLogging的定义

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>());
        services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>)));

        services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<LoggerFilterOptions>>(
            new DefaultLoggerLevelConfigureOptions(LogLevel.Information)));

        configure(new LoggingBuilder(services));
        return services;
    }
}

我们再看看ILoggerFactory的定义

public interface ILoggerFactory : IDisposable
{
    ILogger CreateLogger(string categoryName);

    void AddProvider(ILoggerProvider provider);
}

这里我们还可以看看ILogger部分定义

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);
}

EventId eventId意味着每一记录都可以定义一个事件ID,可以传递进入或者自动生成。

运行结果

dbug: carLogger[0]
      Tesla
dbug: carLogger[3011]
      Tesla
info: carLogger[0]
      Hello Tesla
warn: carLogger[0]
      Hi Warning
fail: carLogger[0]
      Has Error
fail: carLogger[0]
      Has Error
      System.Exception: Hello Car

image

在服务类中引入日志框架

我们可以使用泛型的方法ILogger<OrderService>引入日志到服务类OrderService中。

public class OrderService
{
    ILogger<OrderService> _logger;
    public OrderService(ILogger<OrderService> logger)
    {
        _logger = logger;
    }

    public void Show()
    {
        _logger.LogInformation($"Show Now Time: {DateTime.Now}");
    }
}

Main中将OrderService注入到容器并且获取它调用Show方法。

static void Main(string[] args)
{
    IConfigurationBuilder builder = new ConfigurationBuilder();
    builder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
    var config = builder.Build();

    IServiceCollection services = new ServiceCollection();
    // 使用工厂模式将配置对象注册到容器管理
    services.AddSingleton<IConfiguration>(p => config);
    services.AddTransient<OrderService>();

    services.AddLogging(builder =>
    {
        builder.AddConfiguration(config.GetSection("Logging"));
        builder.AddConsole();
    });

    IServiceProvider serviceProvider = services.BuildServiceProvider();
    var orderService = serviceProvider.GetService<OrderService>();
    orderService.Show();

    Console.ReadKey();
}

先将OrderService服务注入到IServiceCollection管道中。

这里通过IServiceCollectionBuildServiceProvider构建一个服务容器提供方,再通过它获取OrderService服务实例。

输出得到

info: demoForConsole31.Services.OrderService[0]
      Show Now Time: 2022/10/5 23:36:28

如果要给它设置日志级别,这里Key就用它的完整命名空间和类名称demoForConsole31.Services.OrderService

{
    "Console": {
        "LogLevel": {
          "Default": "Information",
          "Program": "Trace",
          "carLogger": "Trace",
          "demoForConsole31.Services.OrderService": "None"
        }
      }
}

这样的话,输出结果就啥都没有了

image

说明针对它的日志级别生效了。

使用模板来输出日志

OrderService服务类的Show方法中,我们要尽量优先使用模板的形式来输出。

public class OrderService
{
    ILogger<OrderService> _logger;
    public OrderService(ILogger<OrderService> logger)
    {
        _logger = logger;
    }

    public void Show()
    {
        // 使用模板
        _logger.LogInformation("Show Now Time: {time}", DateTime.Now);
        // 不使用模板
        _logger.LogInformation($"Show Now Time: {DateTime.Now}");
    }
}

在使用模板的情况下,只有当日志被实际输出的时候,才去拼接数据,而第二种不使用模板的方式会在一开始就拼接数据。

在输出中展示日志

我们发现,在调试的时候,输出窗口是没有日志的,我们可以增加一个Debug的输出,日志框架的就是以一个统一的方式,让我们能够在不同的地方来展示日志。

services.AddLogging(builder =>
{
    builder.AddConfiguration(config.GetSection("Logging"));
    builder.AddConsole();
    builder.AddDebug();
});

image

总结

  • 日志级别的定义,从严重程度的低到高,可以设置最低的日志记录等级
  • 日志对象获取,可以通过ILoggerFactory的方式获取日志对象,对它指定一个名字,也可以通过强类型泛型的模式从容器中获取日志对象。
  • 日志过滤的配置逻辑,针对Log的名称来进行日志的配置。
  • 日志记录的方法
  • 避免记录敏感信息,如密码、密钥等。

日志的作用域

通过日志的作用域可以解决不通请求之间的日志干扰问题。

作用域场景

  • 一个事务包含多条操作时
  • 复杂流程的日志关联时
  • 调用链追踪与请求处理过程对应时

通过ScopeId串联多个日志

我们先设置下配置中的IncludeScopes开关值为true,让Scope机制打开。

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    },
    "Console": {
      "IncludeScopes": true,
      "LogLevel": {
        "Default": "Information",
        "Program": "Trace",
        "carLogger": "Trace",
        "demoForConsole31.Services.OrderService": "Trace"
      }
    }
  }
}

然后使用ILoggerBeginScope方法创建一个作用域和参数

static void Main(string[] args)
{
    IConfigurationBuilder builder = new ConfigurationBuilder();
    builder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
    var config = builder.Build();

    IServiceCollection services = new ServiceCollection();
    // 使用工厂模式将配置对象注册到容器管理
    services.AddSingleton<IConfiguration>(p => config);

    services.AddLogging(builder =>
    {
        builder.AddConfiguration(config.GetSection("Logging"));
        builder.AddConsole();
        builder.AddDebug();
    });

    IServiceProvider serviceProvider = services.BuildServiceProvider();

    var logger = serviceProvider.GetService<ILogger<Program>>();
    using (logger.BeginScope("ScopeId:{scopeId}", Guid.NewGuid()))
    {
        logger.LogDebug("Tesla");
        logger.LogDebug(3011, "Tesla");
        logger.LogInformation("Hello Tesla");
        logger.LogWarning("Hi Warning");
        logger.LogError("Has Error");
        logger.LogError(new Exception("Hello Car"), "Has Error");
    }

    Console.ReadKey();
}

这样输出的日志里面就都会带上一个作用域Id

info: demoForConsole231.Program[0]
      => ScopeId:ed45373b-9dc9-4513-944b-6997e8c2c023
      Hello Tesla
warn: demoForConsole231.Program[0]
      => ScopeId:ed45373b-9dc9-4513-944b-6997e8c2c023
      Hi Warning
fail: demoForConsole231.Program[0]
      => ScopeId:ed45373b-9dc9-4513-944b-6997e8c2c023
      Has Error
fail: demoForConsole231.Program[0]
      => ScopeId:ed45373b-9dc9-4513-944b-6997e8c2c023
      Has Error
      System.Exception: Hello Car

一般情况下,这里ScopeId建议使用唯一标识,如Http的请求Id、SessionId、事务ID、规划的Id。

配置热更新和日志框架配合

static void Main(string[] args)
{
    IConfigurationBuilder builder = new ConfigurationBuilder();
    builder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
    var config = builder.Build();

    IServiceCollection services = new ServiceCollection();
    // 使用工厂模式将配置对象注册到容器管理
    services.AddSingleton<IConfiguration>(p => config);

    services.AddLogging(builder =>
    {
        builder.AddConfiguration(config.GetSection("Logging"));
        builder.AddConsole();
        builder.AddDebug();
    });

    IServiceProvider serviceProvider = services.BuildServiceProvider();

    var logger = serviceProvider.GetService<ILogger<Program>>();
    while(Console.ReadKey().Key != ConsoleKey.Escape)
    {
        using (logger.BeginScope("ScopeId:{scopeId}", Guid.NewGuid()))
        {
            logger.LogDebug("Tesla");
            logger.LogDebug(3011, "Tesla");
            logger.LogInformation("Hello Tesla");
            logger.LogWarning("Hi Warning");
            logger.LogError("Has Error");
            logger.LogError(new Exception("Hello Car"), "Has Error");
        }
        System.Threading.Thread.Sleep(1000);
        Console.WriteLine("-------------------");
    }

    Console.ReadKey();
}

通过一个需要按ESC的While循环,里面还是执行这些输出代码,我们会发现,改变日志配置文件会影响到输出结果

-------------------
info: demoForConsole231.Program[0]
      => ScopeId:88db6653-4132-43b4-88d3-f7ac6a393b27
      Hello Tesla
warn: demoForConsole231.Program[0]
      => ScopeId:88db6653-4132-43b4-88d3-f7ac6a393b27
      Hi Warning
fail: demoForConsole231.Program[0]
      => ScopeId:88db6653-4132-43b4-88d3-f7ac6a393b27
      Has Error
fail: demoForConsole231.Program[0]
      => ScopeId:88db6653-4132-43b4-88d3-f7ac6a393b27
      Has Error
      System.Exception: Hello Car
-------------------
info: demoForConsole231.Program[0]
      Hello Tesla
warn: demoForConsole231.Program[0]
      Hi Warning
fail: demoForConsole231.Program[0]
      Has Error
fail: demoForConsole231.Program[0]
      Has Error
      System.Exception: Hello Car
-------------------

调用链追踪与请求处理过程对应

切换到Asp.Net Core Api的项目。

通过appsettings.jsonConsoleIncludeScope打开。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    },
    "Console": {
      "IncludeScopes": true
    }
  },
  "AllowedHosts": "*"
}

然后在调用的地方输出下日志

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        _logger.LogInformation("Start Logging");
        _logger.LogInformation("End Logging");
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

输出结果里面我们可以看到请求上下文的信息被打印出来了

info: demoForWebApi31.Controllers.WeatherForecastController[0]
      => RequestPath:/weatherforecast RequestId:0HML8B6PSET6N:00000001, SpanId:|13d69064-433d78a1697e56a8., TraceId:13d69064-433d78a1697e56a8, ParentId: => demoForWebApi31.Controllers.WeatherForecastController.Get (demoForWebApi31)
      Start Logging
info: demoForWebApi31.Controllers.WeatherForecastController[0]
      => RequestPath:/weatherforecast RequestId:0HML8B6PSET6N:00000001, SpanId:|13d69064-433d78a1697e56a8., TraceId:13d69064-433d78a1697e56a8, ParentId: => demoForWebApi31.Controllers.WeatherForecastController.Get (demoForWebApi31)
      End Logging

这样我们就可以通过这个上下文信息,把调用链的多条日志关联起来分析了。

结构化日志

好处

  • 易于检索
  • 易于分析统计

场景

  • 实现日志告警
  • 实现上下文的关联
  • 实现与追踪系统的集成

什么是Serilog

https://github.com/serilog/serilog

https://serilog.net

image

Serilog是一个用于.NET应用程序的诊断性日志库。它很容易设置,有一个干净的API,并能在所有最新的.NET平台上运行。虽然它在最简单的应用程序中也很有用,但在对复杂的、分布式的和异步的应用程序和系统进行检测时,Serilog对结构化日志的支持大放异彩。

Serilog Sinks相关Nuget包

Serilog这个日志框架有非常多的官方和第三方实现,这说明它的广泛适应性和流行度。

名称 备注
Serilog.Sinks.Console >= .NET 5.0;
>= .NET Standard 1.3;
>= .NET Framework 4.5;
Serilog.Sinks.Trace >= .NET Standard 1.3;
>= .NET Framework 4.5;
Serilog.Sinks.File >= .NET 5.0;
>= .NET Standard 1.3;
>= .NET Framework 4.5;
Serilog.Sinks.Http >= .NET Standard 2.0;
>= .NET Framework 4.5;
Serilog.Sinks.Debug >= .NET Standard 1.0;
>= .NET Framework 4.5;
Serilog.Sinks.EventLog >= .NET Standard 2.0;
>= .NET Framework 4.5;
Serilog.Sinks.TextWriter >= .NET Standard 1.0;
>= .NET Framework 4.5;
Serilog.Sinks.Map >= .NET 5.0;
>= .NET Standard 1.0;
Serilog.Sinks.Email >= .NET Standard 1.3;
>= .NET Framework 4.5;
Serilog.Sinks.Loggly >= .NET Standard 1.5;
>= .NET Framework 4.5;
Serilog.Sinks.PeriodicBatching >= .NET Standard 1.1;
>= .NET Framework 4.5;
Serilog.Sinks.Async >= .NET Standard 1.1;
>= .NET Framework 4.5;
Serilog.Sinks.Elasticsearch >= .NET Standard 2.0;
Serilog.Sinks.ApplicationInsights >= .NET 6.0;
>= .NET Standard 2.0;
>= .NET Framework 4.6.2;
Serilog.Sinks.Seq >= .NET 5.0;
>= .NET Core 3.1;
>= .NET Standard 1.1;
>= .NET Framework 4.5;
Serilog.Sinks.MSSqlServer >= .NET Core 3.1;
>= .NET Standard 2.0;
>= .NET Framework 4.6.2;
Serilog.Sinks.MSSqlServerCore >= .NET Standard 1.2;
>= .NET Framework 4.5;
Serilog.Sinks.MySQL >= .NET 5.0;
>= .NET Framework 4.5.2;
Serilog.Sinks.MongoDB >= .NET Standard 2.0;
>= .NET Framework 4.7.2;
Serilog.Sinks.PostgreSQL >= .NET 5.0;
>= .NET Standard 2.0;
>= .NET Framework 4.5;
Serilog.Sinks.PostgreSQL >= .NET 5.0;
>= .NET Standard 2.0;
>= .NET Framework 4.5;
Serilog.Sinks.SQLite >= .NET 5.0;
>= .NET Framework 4.5.2;
Serilog.Sinks.Redis.Core >= .NET Standard 2.0;
>= .NET Framework 4.6.1;
Serilog.Sinks.Redis.List >= .NET Standard 2.0;
>= .NET Framework 4.6.1;
Serilog.Sinks.RabbitMQ >= .NET Core 2.0;
>= .NET Standard 2.0;
>= .NET Framework 4.7.2;
Serilog.Sinks.Confluent.Kafka >= .NET Standard 2.0;
Serilog.Sinks.SyslogMessages >= .NET Core 3.1;
>= .NET Standard 2.0;
>= .NET Framework 4.6.2;
Serilog.Sinks.Datadog.Logs >= .NET 5.0;
>= .NET Standard 1.3;
>= .NET Framework 4.5;
Serilog.Sinks.Splunk >= .NET Standard 2.0;
Serilog.Sinks.AwsCloudWatch >= .NET Standard 2.0;
Serilog.Sinks.XUnit >= .NET Standard 2.0;
>= .NET Framework 4.6.1;
Serilog.Sinks.XUnit2 >= .NET Standard 2.1;
Serilog.Sinks.NUnit >= .NET Standard 1.6;
>= .NET Framework 4.6.1;
Serilog.Sinks.Exceptionless >= .NET Standard 2.0;
>= .NET Framework 4.5.2;
Serilog.Sinks.Graylog >= .NET 6.0;
>= .NET Standard 2.0;
>= .NET Framework 4.6;
Serilog.Sinks.Graylog.Batching >= .NET Standard 2.0;
>= .NET Framework 4.6;
Serilog.Sinks.AzureAnalytics >= .NET 5.0;
>= .NET Framework 4.5.2;
Serilog.Sinks.AzureTableStorage >= .NET Standard 2.0;
Serilog.Sinks.AzureBlobStorage >= .NET Standard 2.0;
Serilog.Sinks.AzureEventHub >= .NET Standard 2.0;
>= .NET Framework 4.6.1;
Serilog.Sinks.GoogleCloudLogging >= .NET 5.0;
>= .NET Standard 2.1;
Serilog.Sinks.TestCorrelator >= .NET Standard 2.0;
>= .NET Framework 4.6;
Serilog.Sinks.Raygun >= .NET Standard 2.0;
>= .NET Framework 4.6;
Serilog.Sinks.Slack >= .NET Standard 1.1;
>= .NET Framework 4.5;
Serilog.Sinks.Udp >= .NET Standard 1.3;
>= .NET Framework 4.6.1;
Serilog.Sinks.RollingFileAlternate >= .NET Standard 1.6;
>= .NET Framework 4.5;
Serilog.Sinks.Fluentd >= .NET Standard 1.3;
>= .NET Framework 4.5;
Serilog.Sinks.SumoLogic >= .NET Standard 1.5;
>= .NET Framework 4.5;
Serilog.Sinks.Network >= .NET Standard 1.3;
Serilog.Sinks.Observable >= .NET Standard 1.0;
>= .NET Framework 4.5;
Serilog.Sinks.Grafana.Loki >= .NET Standard 2.0;
Serilog.Sinks.NewRelic.Logs >= .NET Standard 2.0;
Serilog.Sinks.Loki >= .NET Standard 1.3;
Serilog.Sinks.AzureApp >= .NET Standard 2.0;
Serilog.Sinks.Stackify >= .NET Standard 1.3;
>= .NET Framework 4.5;
Serilog.Sinks.InMemory >= .NET Standard 2.0;
Serilog.Sinks.ILogger >= .NET Standard 2.0;
Serilog.Sinks.Logz.Io >= .NET 5.0;
>= .NET Standard 2.0;
>= .NET Framework 4.6.1;
Serilog.Sinks.NLog >= .NET Standard 1.3;
>= .NET Framework 4.5;
Serilog.Sinks.MicrosoftTeams >= .NET Standard 1.1;
Serilog.Sinks.LogstashHttp >= .NET Standard 1.3;
Serilog.Sinks.ElmahIo >= .NET Standard 1.3;
>= .NET Framework 4.5;
Serilog.Sinks.ContextRollingFile >= .NET Core 1.0;
>= .NET Framework 4.5.1;

通过Serilog记录结构化日志

依赖包

https://www.nuget.org/packages/Serilog.AspNetCore/

dotnet add package Serilog.AspNetCore

image

Program.cs中先获取配置框架。

public static IConfiguration Configuration { get; } = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory()).
    AddJsonFile("appsettings.json", optional: false, reloadOnChange: true).
    AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: false, reloadOnChange: true).
    AddEnvironmentVariables().
    Build();

然后在初始化Serilog的时候将上诉配置初始化进去以便接管日志。

public static int Main(string[] args)
{
    Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(Configuration)
        .MinimumLevel.Debug()
        .Enrich.FromLogContext()
        .WriteTo.Console(new RenderedCompactJsonFormatter())
        .WriteTo.File(formatter: new CompactJsonFormatter(), "logs\\carapp.txt", rollingInterval: RollingInterval.Day)
        .CreateLogger();

    try
    {
        Log.Information("Start Web Host");
        CreateHostBuilder(args).Build().Run();
        return 0;
    }
    catch (Exception ex)
    {
        Log.Error(ex, "Host terminal unexpectedly");
        return 0;
    }
    finally
    {
        Log.CloseAndFlush();
    }
}

这里使用了CompactJsonFormatter这样一种格式输出

属性 名称 描述 是否必选
@t Timestamp An ISO 8601 timestamp Yes
@m Message A fully-rendered message describing the event
@mt Message Template Alternative to Message; specifies a message template over the event's properties that provides for rendering into a textual description of the event
@l Level An implementation-specific level identifier (string or number) Absence implies "informational"
@x Exception A language-dependent error representation potentially including backtrace
@i Event id An implementation specific event id (string or number)
@r Renderings If @mt includes tokens with programming-language-specific formatting, an array of pre-rendered values for each such token May be omitted; if present, the count of renderings must match the count of formatted tokens exactly
{"@t":"2016-06-07T03:44:57.8532799Z","@mt":"Hello, {@User}, {N:x8} at {Now}","@r
":["0000007b"],"User":{"Name":"nblumhardt","Tags":[1,2,3]},"N":123,"Now":2016-06
-07T13:44:57.8532799+10:00}

默认Json格式会变成:

{"Timestamp":"2016-06-07T13:44:57.8532799+10:00","Level":"Information","MessageT
emplate":"Hello, {@User}, {N:x8} at {Now}","Properties":{"User":{"Name":"nblumha
rdt","Tags":[1,2,3]},"N":123,"Now":"2016-06-07T13:44:57.8532799+10:00"},"Renderi
ngs":{"N":[{"Format":"x8","Rendering":"0000007b"}]}}

这里将日志不仅输出到Console还输出到指定文件logs\\carapp.txt,接下来在CreateHostBuilder前后记录日志。

{
    "Serilog": {
        "WriteTo": [
            {
                "Name": "Console"
            },
            {
                "Name": "File",
                "Args": {
                    "path": "logs/carapp.txt",
                    "rollingInterval": "Day"
                }
            }
        ]
    }
}

最后还需要在CreateHostBuilder里面通过UseSerilog来完成它的注册,这里将dispose设置为true,会在退出时帮我们是否Serilog对象。

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        .UseSerilog(dispose: true);

查看一下输出。

image

这里看到一行行的Json格式的日志

{
    "@t": "2022-10-07T12:52:36.9558097Z",
    "@m": "Executed action \"demoForSerilog31.Controllers.WeatherForecastController.Get (demoForSerilog31)\" in 118.92ms",
    "@i": "afa2e885",
    "ActionName": "demoForSerilog31.Controllers.WeatherForecastController.Get (demoForSerilog31)",
    "ElapsedMilliseconds": 118.92,
    "EventId": {
        "Id": 2,
        "Name": "ActionExecuted"
    },
    "SourceContext": "Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker",
    "ActionId": "b444ef7e-fe95-4d44-849d-b437a376c436",
    "RequestId": "0HML8CB7A9J7O:00000001",
    "RequestPath": "/weatherforecast",
    "SpanId": "|53e5c988-477ba1153680d4c9.",
    "TraceId": "53e5c988-477ba1153680d4c9",
    "ParentId": ""
}

同时我们可以看到logs目录已经生成了对应于日志文件。

image

在Serilog模式下记录服务日志

[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
    _logger.LogInformation("Api Get");
    var rng = new Random();
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
    .ToArray();
}

还是按之前的方式来记录日志即可,最终输出如下:

{
    "@t": "2022-10-07T12:52:36.9019582Z",
    "@m": "Api Get",
    "@i": "863a0301",
    "SourceContext": "demoForSerilog31.Controllers.WeatherForecastController",
    "ActionId": "b444ef7e-fe95-4d44-849d-b437a376c436",
    "ActionName": "demoForSerilog31.Controllers.WeatherForecastController.Get (demoForSerilog31)",
    "RequestId": "0HML8CB7A9J7O:00000001",
    "RequestPath": "/weatherforecast",
    "SpanId": "|53e5c988-477ba1153680d4c9.",
    "TraceId": "53e5c988-477ba1153680d4c9",
    "ParentId": ""
}

通过配置调整Serilog的日志记录

Serilog的日志配置是单独的节点

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "Serilog": {
      "MinimumLevel": {
        "Default": "Information",
        "Override": {
          "Microsoft": "Error",
          "System": "Information"
        }
      }
    },
    "AllowedHosts": "*"
}

设置的是最小日志级别MinimumLevel,其中Default值是Information,下面的Override代表对前面的Logging下属的LogLevel进行重写覆盖。

这里我们将Microsoft的日志级别改成Error,然后运行发现,果然微软框架的那些日志就没有输出了。

image

{"@t":"2022-10-07T13:05:06.3393350Z","@mt":"Start Web Host"}
{"@t":"2022-10-07T13:05:09.1901033Z","@mt":"Api Get","SourceContext":"demoForSerilog31.Controllers.WeatherForecastController","ActionId":"bb552042-bf89-468e-89c8-a0d00ca6b749","ActionName":"demoForSerilog31.Controllers.WeatherForecastController.Get (demoForSerilog31)","RequestId":"0HML8CI7J3EMV:00000001","RequestPath":"/weatherforecast","SpanId":"|f2c709e3-46c8fcc69580056e.","TraceId":"f2c709e3-46c8fcc69580056e","ParentId":""}

另一种方式使用Serilog日志框架

appsettings.json

这里通过Override将系统自带的日志降低到警告级别。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "Serilog": {
    "MinimumLevel": {
      "Default": "Debug",
      "Override": {
        "Microsoft.AspNetCore": "Warning"
      }
    },
    "WriteTo": [
      { "Name": "Console" }
    ]
  },
  "AllowedHosts": "*"
}

Program.csCreateHostBuilder方法中使用UseSerilog,并且通过泛型方法将上下文的配置注入进去。

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        .UseSerilog((context, logger) =>
        {
            logger.ReadFrom.Configuration(context.Configuration);
        });

与此同时,我们还可以引入Serilog的中间件管道,在Startup.csConfigure方法中配置它UseSerilogRequestLogging

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseSerilogRequestLogging();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

接下来看看输出

[21:28:49 DBG] Hosting starting
[21:28:50 INF] Now listening on: https://localhost:5001
[21:28:50 INF] Now listening on: http://localhost:5000
[21:28:50 INF] Application started. Press Ctrl+C to shut down.
[21:28:50 INF] Hosting environment: Development
[21:28:50 INF] Content root path: C:\TempSpace\HelloLogging\demoForSerilog231
[21:28:50 DBG] Hosting started
[21:28:51 INF] HTTP GET /weatherforecast responded 200 in 197.5633 ms

image

通过seq来收集分析日志

https://hub.docker.com/r/datalust/seq

通过Docker创建seq的实例

docker run --name seq -d --restart unless-stopped -e ACCEPT_EULA=Y -p 5341:80 datalust/seq:2022.1

image

创建成功之后,默认的端口就是5341,我们可以访问http://localhost:5341来查看是否运行成功。

image

接下来,我们需要引入serilog针对seq的包和输出配置

依赖包

https://www.nuget.org/packages/Serilog.Sinks.Seq

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Seq

在serilog的输出配置中增加关于seq的部分,其中有个参数就是填seq服务的地址。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "Serilog": {
    "MinimumLevel": {
      "Default": "Debug",
      "Override": {
        "Microsoft.AspNetCore": "Warning"
      }
    },
    "WriteTo": [
      { "Name": "Console" },
      {
        "Name": "Seq",
        "Args": {
          "serverUrl": "http://localhost:5341/"
        }
      }
    ]
  },
  "AllowedHosts": "*"
}

再次运行一次,即可看到seq中能同步查看日志了。

image

通过Elasticsearch、Kibana来收集和分析日志

因为8.x系列的SSL验证问题,后面的安装全部降级为7.17.6版本,直接不需要验证。

创建一个网络

docker network create somenetwork

https://hub.docker.com/_/elasticsearch

通过Docker来创建ElasticSearch实例

docker run --name elasticsearch -d --restart unless-stopped --net somenetwork -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:8.4.3

image

安装完成之后,因为8.x版本默认开启了SSL认证,可以使用HTTPS的地址访问:https://localhost:9200

默认用户名:elastic,但是密码不知道,好吧,重置下

docker exec -it elasticsearch /bin/bash
cd bin
elasticsearch-reset-password -u elastic
Y

image

得到一个新密码

Password for the [elastic] user successfully reset.
New value: -ON*vo*TpjOVrvFwM3mF

顺便我们生成一下后面Kibana要用到一个Enrollment token

elasticsearch-create-enrollment-token -s kibana

image

eyJ2ZXIiOiI4LjQuMyIsImFkciI6WyIxNzIuMTguMC4zOjkyMDAiXSwiZmdyIjoiOTJlYmRmOGU1MmY5MGY1YWYxMzdiM2FkMTE2MjIzYjk4Njg1ZDFlYmQ2Njc5MGQzNDUzN2Q1ODg5Y2Y3M2FmMyIsImtleSI6IjVWNzNzb01CVVB3VzE1Q0I5Q2lfOjF1UE5MajdKUWVHZTdMaVRZSGwtQ2cifQ==

这下就可以登陆进入了

image


{
  "name" : "076205e5b3c9",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "aZJSOCBWQWq7eTeqPqCO8w",
  "version" : {
    "number" : "8.4.3",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "42f05b9372a9a4a470db3b52817899b99a76ee73",
    "build_date" : "2022-10-04T07:17:24.662462378Z",
    "build_snapshot" : false,
    "lucene_version" : "9.3.0",
    "minimum_wire_compatibility_version" : "7.17.0",
    "minimum_index_compatibility_version" : "7.0.0"
  },
  "tagline" : "You Know, for Search"
}

这里我们还可以从这个实例的日志里面看到几个重要信息

localAddress=/172.18.0.3:9200, remoteAddress=/172.18.0.2:41314

https://hub.docker.com/_/kibana

通过Docker来创建Kibana实例

docker run --name kibana -d --restart unless-stopped --net somenetwork -p 5601:5601 kibana:8.4.3

接下来可通过http://localhost:5601 进入Kibana面板。

image

这里直接填入上面获取到的Enrollment token值。

image

接下来会找你要个验证码。

image

这个验证码其实在这个实例有输出,如果你也是用Docker for Windows,直接点进实例可以看。

image

image

完美验证通过,到了登陆界面,这里账号密码和前面的一致了。

image

依赖包

https://www.nuget.org/packages/Serilog.Sinks.Elasticsearch

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Elasticsearch

image

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        .UseSerilog((context, logger) =>
        {
            logger
            .MinimumLevel.Debug()
            .Enrich.FromLogContext()
            .WriteTo.Elasticsearch(new ElasticsearchSinkOptions())
            .WriteTo.Console();
        });

这个ElasticsearchSinkOptions其实不设置任何东西,它都是有默认值的。

private readonly Uri _defaultNode = new Uri("http://localhost:9200");

public ElasticsearchSinkOptions()
{
    IndexFormat = "logstash-{0:yyyy.MM.dd}";
    DeadLetterIndexName = "deadletter-{0:yyyy.MM.dd}";
    TypeName = DefaultTypeName;
    Period = TimeSpan.FromSeconds(2.0);
    BatchPostingLimit = 50;
    SingleEventSizePostingLimit = null;
    TemplateName = "serilog-events-template";
    ConnectionTimeout = TimeSpan.FromSeconds(5.0);
    EmitEventFailure = EmitEventFailureHandling.WriteToSelfLog;
    RegisterTemplateFailure = RegisterTemplateRecovery.IndexAnyway;
    QueueSizeLimit = 100000;
    BufferFileCountLimit = 31;
    BufferFileSizeLimitBytes = 104857600L;
    FormatStackTraceAsArray = false;
    ConnectionPool = new SingleNodeConnectionPool(_defaultNode);
}

默认就是去连http://localhost:9200这个节点和端口,这也是实例的默认值了,然后默认索引的名字就是logstash-{0:yyyy.MM.dd}

[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
    _logger.LogInformation("Hello ELK");
    var rng = new Random();
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
    .ToArray();
}

这时候我们需要去Kibana里面找一个索引logstash-{0:yyyy.MM.dd}

image

找到右侧的Index Patterns,就按粗一点的筛选logstash-*,点击创建即可

image

image

接下来就可以去AnalyticsDiscover那里了,这时候你就可以看到日志了,完美。

image

解决ElasticSearch v8.x错误问题

从Docker实例中把Elastic自签发的根证书拷贝出来。

docker cp elasticsearch:/usr/share/elasticsearch/config/certs/http_ca.crt C:\Users\xxxxxxxxx\Desktop\http_ca.crt

image

双击它安装到受信任的根证书颁发机构

image

image

image

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        .UseSerilog((hostBuilderContext, loggerConfiguration) =>
        {
            loggerConfiguration
            .WriteTo.Console()
            .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("https://username:password@localhost:9200"))
            {
                TypeName = null,
                AutoRegisterTemplate = true
            });
            SelfLog.Enable(Console.Error);
        });

如果不用代码,使用配置这么写:

{
    "WriteTo": [
        {
            "Name": "Elasticsearch",
            "Args": {
                "nodeUris": "https://username:password@localhost:9200",
                "TypeName": null
            }
        }
    ]
}

这里注意,如果没有TypeName = null会遇到错误:

2022-10-08T11:07:42.8644293Z Caught exception while preforming bulk operation to Elasticsearch: Elasticsearch.Net.ElasticsearchClientException: Request failed to execute. Call: Status code 400 from: POST /_bulk. ServerError: Type: illegal_argument_exception Reason: "Action/metadata line [1] contains an unknown parameter [_type]"
   at Elasticsearch.Net.Transport`1.HandleElasticsearchClientException(RequestData data, Exception clientException, IElasticsearchResponse response)
   at Elasticsearch.Net.Transport`1.FinalizeResponse[TResponse](RequestData requestData, IRequestPipeline pipeline, List`1 seenExceptions, TResponse response)
   at Elasticsearch.Net.Transport`1.RequestAsync[TResponse](HttpMethod method, String path, CancellationToken cancellationToken, PostData data, IRequestParameters requestParameters)
   at Serilog.Sinks.Elasticsearch.ElasticsearchSink.EmitBatchAsync(IEnumerable`1 events)

如果没有安装好这个根证书会遇到错误:

Caught exception while preforming bulk operation to Elasticsearch: Elasticsearch.Net.ElasticsearchClientException: The SSL connection could not be established, see inner exception..
Call: Status code unknown from: POST /_bulk
 ---> System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
 ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: UntrustedRoot

这时候再次启动程序,就可以在Kibana里面看到我们的上传的索引了。

image

创建Kibana的Data Views

image

image

接下来就可以去Analytics的Discover那里查看我们这个过滤条件的日志了。

image

稍微优化一下代码和日志配置

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft.AspNetCore": "Warning"
      }
    }
  },
  "AllowedHosts": "*"
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        .UseSerilog((hostBuilderContext, loggerConfiguration) =>
        {
            loggerConfiguration.ReadFrom.Configuration(hostBuilderContext.Configuration);
            loggerConfiguration
            .Enrich.FromLogContext()
            .WriteTo.Console()
            .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("https://xxx:xxxxxxxxx@localhost:9200"))
            {
                TypeName = null,
                AutoRegisterTemplate = true
            });
            SelfLog.Enable(Console.Error);
        });
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseSerilogRequestLogging();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

运行效果

image

image

通过Azure Blob Storage来收集和分析日志

如果你有一个Azure Blob Storage账号更好,如果没有可以通过一个容器实例来模拟

Azurite开源仿真器提供一个免费的本地环境,用于测试Azure Blob、队列存储和表存储应用程序。如果你对应用程序在本地的工作状况感到满意,可以改用云中的Azure存储帐户。该仿真器在Windows、Linux和macOS上提供跨平台支持。

通过Docker来创建Azurite实例

docker run --name azurite -d --restart unless-stopped -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite

image

依赖包

https://www.nuget.org/packages/Serilog.Sinks.AzureBlobStorage

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.AzureBlobStorage

image

使用代码

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
    .UseSerilog((hostBuilderContext, loggerConfiguration) =>
    {
        loggerConfiguration.ReadFrom.Configuration(hostBuilderContext.Configuration);
        loggerConfiguration
        .Enrich.FromLogContext()
        .WriteTo.Console()
        .WriteTo.AzureBlobStorage("DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;", LogEventLevel.Information);
        SelfLog.Enable(Console.Error);
    });

运行结果,通过Azure 存储资源管理器可以看到,日志被成功写道了我们的Azure Blob Storage仿真器中。

image

这里AzureBlobStorage静态扩展方向有好几个缺省参数,可用于自定义。

public static LoggerConfiguration AzureBlobStorage(this LoggerSinkConfiguration loggerConfiguration, string connectionString, LogEventLevel restrictedToMinimumLevel = LogEventLevel.Verbose, string storageContainerName = null, string storageFileName = null, string outputTemplate = null, bool writeInBatches = false, TimeSpan? period = null, int? batchPostingLimit = null, bool bypassBlobCreationValidation = false, IFormatProvider formatProvider = null, ICloudBlobProvider cloudBlobProvider = null, long? blobSizeLimitBytes = null, int? retainedBlobCountLimit = null, bool useUtcTimeZone = false)
{
    if (loggerConfiguration == null)
    {
        throw new ArgumentNullException("loggerConfiguration");
    }

    if (string.IsNullOrEmpty(connectionString))
    {
        throw new ArgumentNullException("connectionString");
    }

    if (string.IsNullOrEmpty(outputTemplate))
    {
        outputTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}";
    }

    return loggerConfiguration.AzureBlobStorage(new MessageTemplateTextFormatter(outputTemplate, formatProvider), connectionString, restrictedToMinimumLevel, storageContainerName, storageFileName, writeInBatches, period, batchPostingLimit, bypassBlobCreationValidation, cloudBlobProvider, blobSizeLimitBytes, retainedBlobCountLimit, useUtcTimeZone);
}
.UseSerilog((hostBuilderContext, loggerConfiguration) =>
{
    loggerConfiguration.ReadFrom.Configuration(hostBuilderContext.Configuration);
    loggerConfiguration
    .Enrich.FromLogContext()
    .WriteTo.Console()
    .WriteTo.AzureBlobStorage
    (
        connectionString: "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;", 
        LogEventLevel.Information,
        storageContainerName: "abswithserilogs",
        storageFileName: "abswithserilog-{yyyy}-{MM}-{dd}.txt"
    );
    SelfLog.Enable(Console.Error);
});

运行结果

image

参考

posted @ 2022-10-03 01:49  TaylorShi  阅读(988)  评论(0编辑  收藏  举报