乘风破浪,遇见最佳跨平台跨终端框架.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
社区第三方日志框架
社区中实现了对
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
实践理解
准备示例项目
dotnet new sln -o HelloLogging
dotnet new console -o demoForConsole31 -f netcoreapp3.1
dotnet sln add .\demoForConsole31\demoForConsole31.csproj
explorer.exe .
准备配置文件
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
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
在服务类中引入日志框架
我们可以使用泛型的方法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
管道中。
这里通过IServiceCollection
的BuildServiceProvider
构建一个服务容器提供方,再通过它获取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"
}
}
}
这样的话,输出结果就啥都没有了
说明针对它的日志级别生效了。
使用模板来输出日志
在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();
});
总结
- 日志级别的定义,从严重程度的低到高,可以设置最低的日志记录等级
- 日志对象获取,可以通过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"
}
}
}
}
然后使用ILogger
的BeginScope
方法创建一个作用域和参数
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.json
把Console
的IncludeScope
打开。
{
"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
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记录结构化日志
依赖包
dotnet add package Serilog.AspNetCore
在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);
查看一下输出。
这里看到一行行的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
目录已经生成了对应于日志文件。
在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
,然后运行发现,果然微软框架的那些日志就没有输出了。
{"@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.cs
的CreateHostBuilder
方法中使用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.cs
的Configure
方法中配置它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
通过seq来收集分析日志
通过Docker创建seq的实例
docker run --name seq -d --restart unless-stopped -e ACCEPT_EULA=Y -p 5341:80 datalust/seq:2022.1
创建成功之后,默认的端口就是5341
,我们可以访问http://localhost:5341来查看是否运行成功。
接下来,我们需要引入serilog针对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中能同步查看日志了。
通过Elasticsearch、Kibana来收集和分析日志
因为8.x系列的SSL验证问题,后面的安装全部降级为7.17.6版本,直接不需要验证。
创建一个网络
docker network create somenetwork
通过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
安装完成之后,因为8.x版本默认开启了SSL认证,可以使用HTTPS
的地址访问:https://localhost:9200
默认用户名:elastic
,但是密码不知道,好吧,重置下
docker exec -it elasticsearch /bin/bash
cd bin
elasticsearch-reset-password -u elastic
Y
得到一个新密码
Password for the [elastic] user successfully reset.
New value: -ON*vo*TpjOVrvFwM3mF
顺便我们生成一下后面Kibana要用到一个Enrollment token
elasticsearch-create-enrollment-token -s kibana
eyJ2ZXIiOiI4LjQuMyIsImFkciI6WyIxNzIuMTguMC4zOjkyMDAiXSwiZmdyIjoiOTJlYmRmOGU1MmY5MGY1YWYxMzdiM2FkMTE2MjIzYjk4Njg1ZDFlYmQ2Njc5MGQzNDUzN2Q1ODg5Y2Y3M2FmMyIsImtleSI6IjVWNzNzb01CVVB3VzE1Q0I5Q2lfOjF1UE5MajdKUWVHZTdMaVRZSGwtQ2cifQ==
这下就可以登陆进入了
{
"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
通过Docker来创建Kibana实例
docker run --name kibana -d --restart unless-stopped --net somenetwork -p 5601:5601 kibana:8.4.3
接下来可通过http://localhost:5601 进入Kibana面板。
这里直接填入上面获取到的Enrollment token
值。
接下来会找你要个验证码。
这个验证码其实在这个实例有输出,如果你也是用Docker for Windows,直接点进实例可以看。
完美验证通过,到了登陆界面,这里账号密码和前面的一致了。
依赖包
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Elasticsearch
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}
找到右侧的Index Patterns
,就按粗一点的筛选logstash-*
,点击创建即可
接下来就可以去Analytics
的Discover
那里了,这时候你就可以看到日志了,完美。
解决ElasticSearch v8.x错误问题
从Docker实例中把Elastic自签发的根证书拷贝出来。
docker cp elasticsearch:/usr/share/elasticsearch/config/certs/http_ca.crt C:\Users\xxxxxxxxx\Desktop\http_ca.crt
双击它安装到受信任的根证书颁发机构
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里面看到我们的上传的索引了。
创建Kibana的Data Views
接下来就可以去Analytics的Discover那里查看我们这个过滤条件的日志了。
稍微优化一下代码和日志配置
{
"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();
});
}
运行效果
通过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
依赖包
https://www.nuget.org/packages/Serilog.Sinks.AzureBlobStorage
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.AzureBlobStorage
使用代码
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仿真器中。
这里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);
});
运行结果
参考
- 乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 官方扩展集锦(Microsoft.Extensions on Nuget)
- https://github.com/serilog/serilog
- Why Serilog
- .NET Core 日志模型
- .NET Core 日志框架:Serilog
- Microsoft.Extensions.Logging
- serilog-sinks-seq
- serilog-sinks-elasticsearch
- Docker安装Elasticsearch 8.x 、Kibana 8.x等
- docker安装Elasticsearch 8.x步骤
- ASP.NET Core Logging with Azure App Service and Serilog
- Write to ElasticSearch 8 with Serilog in .NET Core
- Error when sink is connected to ElasticSearch 8.0.0-SNAPSHOT
- Install Elasticsearch with Docker - Security certificates and keys
- Write to Azure Blob Storage with Serilog in .NET Core
- 使用Azurite模拟器进行本地Azure存储开发
- Serilog.Sinks.ApplicationInsights
- 玩转ASP.NET Core中的日志组件
- 玩转ASP.NET Core中的日志组件