Serilog
Serilog 是一个 .NET 平台上的强大的日志记录库。它提供了丰富的 API 以及可插拔的日志格式化器和输出器,使得在 .NET 应用程序中实现可定制化的、可扩展的日志记录变得轻而易举。
在本文中,我们将探讨 Serilog 的一些基础知识、API、配置和示例。
# 基础知识
# 日志级别
Serilog 支持多个日志级别,包括以下级别(按照严重程度从高到低排列):
Fatal
: 程序已经无法继续运行,需要立即解决的问题。Error
: 一个错误发生,需要被处理。Warning
: 一个警告,通常需要被留意,但是不需要立即处理。Information
: 提供有用的信息,通常只有在调试应用程序时才需要关注。Debug
: 提供调试信息,有助于调试应用程序。Verbose
: 提供大量的细节信息,通常只用于调试复杂的问题。
# 日志输出
Serilog 支持多种日志输出,包括:
- Console(控制台输出)
- File(文件输出)
- Seq(通过 Seq (opens new window) 输出到日志收集器)
- Elasticsearch(通过 Elasticsearch (opens new window) 输出到搜索引擎)
此外,Serilog 还支持自定义日志输出器。
# 日志格式
Serilog 支持多种日志格式化方式,包括:
- 简单文本格式
- JSON 格式
- Message Templates 格式(一种更加灵活的格式)
# 安装
可以通过 NuGet 安装 Serilog。
Install-Package Serilog
# 使用
# 基础使用
在应用程序中使用 Serilog 很简单。下面的示例演示了如何在控制台输出日志:
using Serilog;
class Program
{
static void Main()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
Log.Information("Hello, Serilog!");
Log.CloseAndFlush();
}
}
上面的代码将 Hello, Serilog!
输出到控制台。
# 详细使用
# 日志级别
下面的示例演示了如何在 Serilog 中设置不同的日志级别:
using Serilog;
class Program
{
static void Main()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Verbose()
.WriteTo.Console()
.CreateLogger();
Log.Verbose("This is a verbose log message.");
Log.Debug("This is a debug log message.");
Log.Information("This is an informational log message.");
Log.Warning("This is a warning log message.");
Log.Error("This is an error log message.");
Log.Fatal("This is a fatal log message.");
Log.CloseAndFlush();
}
}
```csharp
上面的代码将演示如何使用不同的日志级别来记录日志消息。
#### 消息模板
Serilog 采用消息模板来格式化日志消息。下面是一个简单的消息模板示例:
```csharp
using Serilog;
class Program
{
static void Main()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
Log.Information("Hello, {Name}!", "Serilog");
Log.CloseAndFlush();
}
}
在上面的示例中,我们使用了一个带有模板的控制台输出器,并且在消息模板中使用了占位符 {Name}
。当日志记录方法被调用时,{Name}
将被替换为 Serilog
。我们可以看到 Hello, Serilog!
在控制台输出。
# 日志属性
Serilog 支持日志属性,这使得我们可以在日志消息中记录更多的信息。下面的示例演示了如何在日志消息中添加属性:
using Serilog;
class Program
{
static void Main()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
Log.Information("Processed {@Count} records in {Time} ms.", new { Count = 10, Time = 123 });
Log.CloseAndFlush();
}
}
在上面的示例中,我们使用了一个匿名类型来表示日志属性。在日志消息中,我们使用了 @
符号来引用这个匿名类型。当日志记录方法被调用时,Serilog 将自动将匿名类型的属性添加到日志消息中。上面的代码将输出 Processed { Count: 10, Time: 123 } records in 0 ms.
。
# 输出模板定义
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
这个也是官方的默认模板,我们可以这个扩展
.WriteTo.File("log.txt",
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
# 输出到文件
rollingInterval: RollingInterval.Day 每天一个日志文件
outputTemplate 输出格式模板
.WriteTo.File(
$"logs\\log-.txt" ,
//每天一个文件 ,生成类似:log-20230914.txt
//fileSizeLimitBytes: null 文件限制大小:无 如果不配置默认是:1GB 1GB就不记录了
//retainedFileCountLimit: null 保留文件数量 如果不配置只保留31天的日志
//https://github.com/serilog/serilog-sinks-file
rollingInterval: RollingInterval.Day , retainedFileCountLimit: null ,
//fileSizeLimitBytes: null,
//单个文件大小: 1024000 1024000是1M
//rollOnFileSizeLimit: true 就是滚动文件,如果超过单个文件大小,会滚动文件 产生类似:log.txt log_001.txt log_002.txt
fileSizeLimitBytes: 3024000 , rollOnFileSizeLimit: true ,
//非必填:指定最小等级
restrictedToMinimumLevel: LogEventLevel.Information ,
//非必填: 也可以指定输出格式:这种格式好像与系统默认没有什么区别
//outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
//outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception} {NewLine}{Version}{myval}"
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception} {NewLine}{Version}{myval} {NewLine}{UserId}{myid}{NewLine}"
)
# 结构化记录日志
# 写入变量
var itemNumber = 10;
var itemCount = 999;
// 使用占位符写入
// 结果:2023-09-15 22:23:54.576 +08:00 [INF] Processing item 10 of 999
this._logger.LogDebug( "Processing item {ItemNumber} of {ItemCount}" , itemNumber , itemCount );
# 写入类
特别提示:记录对象后 1.日志中会多一个$type属性 2.日期类型数据格式化后都是这样格式:2023-09-16T22:26:27.5905512+08:00
var wf = new WeatherForecast
{
Date = DateTime.Now.AddDays( 1 ) ,
TemperatureC = 55 ,
Summary = ""
};
// @表示一个对象 这样就可以把一个对象直接传递进去
// 特别提示:记录对象后
// 1.日志中会多一个$type属性
// 2.日期类型数据格式化后都是这样格式:2023-09-16T22:26:27.5905512+08:00
//结果:2023-09-15 22:26:27.601 +08:00 [INF] WeatherForecast 的数据 {"Date":"2023-09-16T22:26:27.5905512+08:00","TemperatureC":55,"TemperatureF":130,"Summary":"","$type":"WeatherForecast"}
this._logger.LogInformation( "WeatherForecast 的数据 {@wf}" , wf );
# 写入集合
List<string> list1 = new List<string>() { "q1" , "q2" };
//写入集合
//结果:2023-09-15 22:36:46.751 +08:00 [INF] 集合的数据 ["q1","q2"]
this._logger.LogInformation( "集合的数据 {@list1}" , list1 );
List<WeatherForecast> listw = new List<WeatherForecast>() {
new WeatherForecast
{
Date = DateTime.Now.AddDays( 1 ) ,
TemperatureC = 11 ,
Summary = "one"
},
new WeatherForecast
{
Date = DateTime.Now.AddDays( 2 ) ,
TemperatureC = 22 ,
Summary = "two"
}};
//写入集合
//结果: 2023-09-15 22:39:53.863 +08:00 [INF] 集合的数据 [{"Date":"2023-09-16T22:39:53.8634787+08:00","TemperatureC":11,"TemperatureF":51,"Summary":"one","$type":"WeatherForecast"},{"Date":"2023-09-17T22:39:53.8634842+08:00","TemperatureC":22,"TemperatureF":71,"Summary":"two","$type":"WeatherForecast"}]
this._logger.LogInformation( "集合的数据 {@listw}" , listw );
# 写入匿名类
var user = new
{
Name = "Nick" ,
Id = "nblumhardt" ,
add = new List<string>() { "add1" , "add2" } ,
man = new
{
age = 1 ,
names = "qq"
}
};
// @表示一个对象(上面这个是匿名类也可以写入的)
//结果:2023-09-15 22:23:54.576 +08:00 [INF] Logged on user {"Name":"Nick","Id":"nblumhardt","add":["add1","add2"],"man":{"age":1,"names":"qq"}}
this._logger.LogInformation( "Logged on user {@user}" , user );
# 输出上下文
# 方式1:固定值的上下文
Log.Logger = new LoggerConfiguration()
// 注册日志上下文
.Enrich.FromLogContext()
// 上下文
.Enrich.WithProperty( "Version" , "1.0.0" )
.Enrich.WithProperty( "myval" , 123 )
配置模版定义上下文
//outputTemplate中配置写入上下文
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception} {NewLine}{Version}{myval}"
# 方式2:动态传递上下文
配置模版定义上下文
outputTemplate中定义了2个上下文:UserId和myid
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception} {NewLine}{Version}{myval} {NewLine}{UserId}{myid}{NewLine}"
代码传递值
//利用BeginScope传递上下文
using ( this._logger.BeginScope( new Dictionary<string , object>
{
["UserId"] = "svrooij" ,
["myid"] = 123456 ,
} ) )
{
this._logger.LogInformation( "我传递上下文参数过来了" );
}
//不是每个都要传递上下文,没有传递也不报错
this._logger.LogError( new Exception( "0异常啦" ) , "我自己造的" );
this._logger.LogInformation( new Random().Next( 10000 ).ToString() );
# 日志过滤器
Serilog 支持过滤器来控制日志输出。下面的示例演示了如何使用过滤器来仅输出错误级别以上的日志:
using Serilog;
class Program
{
static void Main()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.Filter.ByIncludingOnly(logEvent => logEvent.Level >= LogEventLevel.Error)
.CreateLogger();
Log.Verbose("This is a verbose log message.");
Log.Debug("This is a debug log message.");
Log.Information("This is an informational log message.");
Log.Warning("This is a warning log message.");
Log.Error("This is an error log message.");
Log.Fatal("This is a fatal log message.");
Log.CloseAndFlush();
}
}
在上面的示例中,我们使用了 ByIncludingOnly
过滤器来仅输出错误级别以上的日志。
# 几种常用过滤方式
Filter.ByIncludingOnly:是包括
Filter.ByExcluding:是不包括
# a.不同等级,输出不同文件
Func<LogEvent , bool> isInformation = ( logEvent ) => logEvent.Level == LogEventLevel.Information;
Func<LogEvent , bool> isError = ( logEvent ) => logEvent.Level == LogEventLevel.Error;
// 输出到文件
//不同等级,输出不同文件
.WriteTo.Logger( lg =>
{
lg.Filter.ByIncludingOnly( p => isInformation( p ) )
.WriteTo.File( $"logs\\log_Information-.txt" , rollingInterval: RollingInterval.Day );
} )
.WriteTo.Logger( lg =>
{
lg.Filter.ByIncludingOnly( p => isError( p ) )
.WriteTo.File( $"logs\\log_Error-.txt" , rollingInterval: RollingInterval.Day );
} )
# b.不同类,输出不同文件
//不同类,输出不同文件
.WriteTo.Logger( lg =>
{
lg.Filter.ByIncludingOnly( Matching.FromSource<WeatherForecastController>() )
.WriteTo.File( $"logs\\WeatherForecastController-.txt" , rollingInterval: RollingInterval.Day );
} )
.WriteTo.Logger( lg =>
{
lg.Filter.ByIncludingOnly( Matching.FromSource<ValuesController>() )
.WriteTo.File( $"logs\\ValuesController-.txt" , rollingInterval: RollingInterval.Day );
} )
# c.根据消息内容过滤,输出不同文件
Func<LogEvent , bool> isbsWeatherForecastController = ( p ) =>
{
var msg = p.RenderMessage();
if ( !string.IsNullOrEmpty( msg ) )
{
return msg.Contains( "bs:WeatherForecastController" , StringComparison.OrdinalIgnoreCase );
}
return false;
};
//根据消息内容过滤,输出不同文件
.WriteTo.Logger( lg =>
{
lg.Filter.ByIncludingOnly( p => isbsWeatherForecastController( p ) )
.Filter.ByIncludingOnly( isMidServices )
.WriteTo.File( $"logs\\WeatherForecastController-.txt" , rollingInterval: RollingInterval.Day );
} )
# d.根据上下文值,输出不同文件
//根据上下文值,输出不同文件
.WriteTo.Logger( lg =>
{
//lg.Filter.ByIncludingOnly( Matching.WithProperty( "UserId" , "svrooij" ) )
//下面这个,也是等效写法
lg.Filter.ByIncludingOnly( Matching.WithProperty<string>( "UserId" , str => !string.IsNullOrEmpty( str ) && str.Equals( "svrooij" , StringComparison.OrdinalIgnoreCase ) ) )
.WriteTo.File( $"logs\\WeatherForecastController-.txt" ,
rollingInterval: RollingInterval.Day ,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception} {NewLine}{UserId}{myid}{NewLine}" );
} )
# e.实现接口ILogEventFilter
public class CustomFilter : ILogEventFilter
{
private readonly string _propertyName;
private readonly string _propertyValue;
public CustomFilter ( string propertyName , string propertyValue )
{
_propertyName = propertyName;
_propertyValue = propertyValue;
}
public bool IsEnabled ( LogEvent logEvent )
{
//下面的判断逻辑是:Information等级的,MidServices类产生的,判断某个上下文是否为某个值
if ( logEvent.Level == LogEventLevel.Information )
{
var f = Matching.FromSource<MidServices>();
bool bl = f.Invoke( logEvent );
if ( bl )
{
var f2 = Matching.WithProperty<string>( _propertyName ,
str => !string.IsNullOrEmpty( str ) && str.Equals( _propertyValue , StringComparison.OrdinalIgnoreCase ) );
bool bl2 = f2.Invoke( logEvent );
if ( bl2 )
{
return true;
}
}
}
return false;
}
}
# 配置文件
Serilog 还支持使用配置文件来配置日志记录。下面是一个配置文件示例:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="serilog:minimum-level" value="Verbose" />
<add key="serilog:write-to:Console" />
</appSettings>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
</configuration>
在上面的示例中,我们使用了一个配置文件来配置 Serilog。我们将日志级别设置为 Verbose
,并且将输出器设置为 Console
。
# 扩展
Serilog 提供了丰富的扩展方式,包括:
- 日志输出器扩展
- 日志过滤器扩展
- 日志格式化器扩展
# 日志输出器扩展
Serilog 提供了多种扩展方式来添加自定义日志输出器。
# 控制台输出器扩展
以下示例演示了如何添加一个自定义的控制台输出器:
using Serilog;
using Serilog.Configuration;
using Serilog.Events;
using Serilog.Formatting;
public static class CustomConsoleSinkExtensions
{
public static LoggerConfiguration CustomConsole(
this LoggerSinkConfiguration sinkConfiguration,
ITextFormatter formatter = null,
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum)
{
return sinkConfiguration.Sink(
new CustomConsoleSink(formatter),
restrictedToMinimumLevel);
}
}
public class CustomConsoleSink : ILogEventSink
{
private readonly ITextFormatter _formatter;
public CustomConsoleSink(ITextFormatter formatter)
{
_formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
}
public void Emit(LogEvent logEvent)
{
var message = new StringWriter();
_formatter.Format(logEvent, message);
Console.WriteLine(message.ToString());
}
}
class Program
{
static void Main()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.CustomConsole()
.CreateLogger();
Log.Information("Hello, Serilog!");
Log.CloseAndFlush();
}
}
在上面的示例中,我们定义了一个名为 CustomConsoleSink
的自定义控制台输出器,并将其添加到 Serilog 的输出器列表中。
# Elasticsearch 输出器扩展
以下示例演示了如何添加一个自定义的 Elasticsearch 输出器:
using Serilog;
using Serilog.Configuration;
using Serilog.Events;
using Serilog.Formatting;
using Serilog.Sinks.Elasticsearch;
public static class CustomElasticsearchSinkExtensions
{
public static LoggerConfiguration CustomElasticsearch(
this LoggerSinkConfiguration sinkConfiguration,
ITextFormatter formatter = null,
ElasticsearchSinkOptions elasticsearchOptions = null,
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum)
{
return sinkConfiguration.Sink(
new CustomElasticsearchSink(formatter, elasticsearchOptions),
restrictedToMinimumLevel);
}
}
public class CustomElasticsearchSink : ElasticsearchSink
{
public CustomElasticsearchSink(
ITextFormatter formatter,
ElasticsearchSinkOptions elasticsearchOptions) :
base(elasticsearchOptions)
{
Formatter = formatter;
}
public ITextFormatter Formatter { get; }
protected override void EmitBatch(
IEnumerable<LogEvent> events,
IBulkWriter writer)
{
foreach (var logEvent in events)
{
var message = new StringWriter();
Formatter.Format(logEvent, message);
var json = message.ToString();
writer.IndexDocument(json);
}
}
}
class Program
{
static void Main()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.CustomElasticsearch(new ElasticsearchSinkOptions(new Uri("[http://localhost:9200](http://localhost:9200/) ")),new ElasticsearchJsonFormatter(),LogEventLevel.Information)
.CreateLogger();
Log.Information("Hello, Serilog!");
Log.CloseAndFlush();
}
}
在上面的示例中,我们定义了一个名为 CustomElasticsearchSink
的自定义 Elasticsearch 输出器,并将其添加到 Serilog 的输出器列表中。
# 日志过滤器扩展
Serilog 提供了多种扩展方式来添加自定义日志过滤器。
以下示例演示了如何添加一个自定义的日志过滤器:
using Serilog;
using Serilog.Configuration;
using Serilog.Events;
public static class CustomFilterExtensions
{
public static LoggerConfiguration CustomFilter(
this LoggerFilterConfiguration filterConfiguration,
string propertyName,
string propertyValue,
LogEventLevel minimumLevel = LevelAlias.Minimum)
{
return filterConfiguration.Add(
new CustomFilter(propertyName, propertyValue),
minimumLevel);
}
}
public class CustomFilter : ILogEventFilter
{
private readonly string _propertyName;
private readonly string _propertyValue;
public CustomFilter(string propertyName, string propertyValue)
{
_propertyName = propertyName;
_propertyValue = propertyValue;
}
public bool IsEnabled(LogEvent logEvent)
{
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
var property = logEvent.Properties[_propertyName];
if (property == null) return false;
return property.ToString().Equals(_propertyValue);
}
}
class Program
{
static void Main()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.Filter.CustomFilter("Environment", "Production")
.CreateLogger();
Log.Information("Hello, Serilog!");
Log.CloseAndFlush();
}
}
在上面的示例中,我们定义了一个名为 CustomFilter
的自定义过滤器,并将其添加到 Serilog 的过滤器列表中。该过滤器将仅输出 Environment
属性为 Production
的日志。
# 日志格式化器扩展
Serilog 提供了多种扩展方式来添加自定义日志格式化器。
以下示例演示了如何添加一个自定义的日志格式化器:
using Serilog;
using Serilog.Configuration;
using Serilog.Events;
using Serilog.Formatting.Display;
public static class CustomFormatterExtensions
{
public static LoggerConfiguration CustomFormatter(
this LoggerConfiguration loggerConfiguration,
string format,
IFormatProvider formatProvider = null,
LogEventLevel minimumLevel = LevelAlias.Minimum)
{
return loggerConfiguration.Sink(
new CustomFormatterSink(format, formatProvider),
minimumLevel);
}
}
public class CustomFormatterSink : ILogEventSink
{
private readonly MessageTemplateTextFormatter _formatter;
public CustomFormatterSink(string format, IFormatProvider formatProvider = null)
{
_formatter = new MessageTemplateTextFormatter(format, formatProvider);
}
public void Emit(LogEvent logEvent)
{
var message = new StringWriter();
_formatter.Format(logEvent, message);
Console.WriteLine(message.ToString());
}
}
class Program
{
static void Main()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.CustomFormatter("Hello, {Name}!", null, LogEventLevel.Information)
.CreateLogger();
Log.Information("{Name}", "Serilog");
Log.CloseAndFlush();
}
}
```markdown
在上面的示例中,我们定义了一个名为 `CustomFormatterSink` 的自定义格式化器,并将其添加到 Serilog 的输出器列表中。该格式化器将 `Hello, {Name}!` 格式化为 `Hello, Serilog!`。
## 结论
Serilog 是一个功能强大的 .NET 平台上的日志记录库。它提供了丰富的 API 以及可插拔的日志格式化器和输出器,使得在 .NET 应用程序中实现可定制化的、可扩展的日志记录变得轻而易举。希望本文对您有所帮助。