MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#中的日志记录与监控框架Serilog

2021-06-237.8k 阅读

Serilog简介

Serilog是一个用于.NET平台的现代化日志记录库,以其简洁的API、丰富的功能以及出色的可扩展性在C#开发者社区中备受青睐。它的设计理念是提供一种清晰、流畅的方式来记录应用程序的各种事件,从简单的信息输出到复杂的诊断跟踪。

Serilog的核心特点之一是其结构化日志记录功能。与传统的文本日志记录不同,结构化日志记录允许将日志数据以一种更有组织、更易于查询和分析的方式存储。例如,当记录一个用户登录事件时,不仅可以记录“用户登录成功”这样的文本信息,还可以同时记录用户名、登录时间、登录IP等结构化数据,这些数据可以在后续进行高效的筛选和分析。

Serilog的安装与配置

在C#项目中使用Serilog,首先需要通过NuGet包管理器安装Serilog及其相关的包。对于一个典型的控制台应用程序,在Visual Studio中,可以通过右键点击项目,选择“管理NuGet程序包”,然后在NuGet包管理器中搜索“Serilog”并安装。

安装完成后,需要在代码中进行基本的配置。以下是一个简单的控制台应用程序中配置Serilog的示例:

using Serilog;

class Program
{
    static void Main()
    {
        Log.Logger = new LoggerConfiguration()
           .WriteTo.Console()
           .CreateLogger();

        try
        {
            Log.Information("应用程序开始运行");
            // 应用程序逻辑代码
        }
        catch (Exception ex)
        {
            Log.Error(ex, "应用程序发生错误");
        }
        finally
        {
            Log.CloseAndFlush();
        }
    }
}

在上述代码中,LoggerConfiguration用于配置Serilog的行为。.WriteTo.Console()表示将日志输出到控制台,.CreateLogger()创建实际的日志记录器实例。在try-catch块中,使用Log.Information记录应用程序开始运行的信息,使用Log.Error记录捕获到的异常信息,并且在应用程序结束时,通过Log.CloseAndFlush确保所有日志都被正确输出并清理资源。

日志级别

Serilog支持多种日志级别,这些级别用于区分日志的重要性和类型。常见的日志级别有:

  1. Verbose:最详细的日志级别,通常用于开发和调试阶段,记录所有可能有用的信息。
  2. Debug:用于调试目的,包含有助于调试应用程序的详细信息,但比Verbose级别稍少一些冗余信息。
  3. Information:用于记录应用程序正常运行过程中的重要事件,如服务启动、用户登录等。
  4. Warning:表示出现了一些可能影响应用程序正常运行的情况,但目前尚未导致错误,如配置文件中的一些不推荐设置。
  5. Error:记录应用程序运行过程中发生的错误,如未处理的异常。
  6. Fatal:表示发生了严重的、导致应用程序无法继续正常运行的错误,如系统崩溃。

可以在配置Serilog时设置日志级别,只有大于或等于设置级别的日志才会被记录。例如,设置日志级别为Warning,则VerboseDebugInformation级别的日志将不会被记录。

Log.Logger = new LoggerConfiguration()
   .MinimumLevel.Warning()
   .WriteTo.Console()
   .CreateLogger();

在上述代码中,.MinimumLevel.Warning()设置了最低日志级别为Warning

结构化日志记录

基本结构化日志

如前文所述,结构化日志记录是Serilog的一大特色。通过在日志消息中嵌入结构化数据,可以使日志更具价值。例如,假设我们有一个用户注册的方法,我们可以这样记录结构化日志:

public void RegisterUser(string username, string email)
{
    var userData = new { Username = username, Email = email };
    Log.Information("用户注册成功", userData);
}

在上述代码中,创建了一个匿名对象userData,包含UsernameEmail属性,然后将其作为第二个参数传递给Log.Information方法。这样,在日志中不仅会记录“用户注册成功”的文本信息,还会包含用户的用户名和邮箱等结构化数据。

复杂结构化日志

对于更复杂的场景,比如记录一个订单处理过程,订单可能包含多个商品、总价、客户信息等。我们可以定义自定义的类来表示这些复杂数据结构,并在日志中使用。

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class Order
{
    public int OrderId { get; set; }
    public List<Product> Products { get; set; }
    public decimal TotalPrice { get; set; }
    public string CustomerName { get; set; }
}

public void ProcessOrder(Order order)
{
    Log.Information("开始处理订单 {Order}", order);
}

在上述代码中,定义了ProductOrder类来表示商品和订单信息。在ProcessOrder方法中,通过Log.Information记录开始处理订单的信息,并将整个order对象作为结构化数据记录下来。这样在日志中可以完整地看到订单的详细信息,包括订单ID、商品列表、总价和客户名称等。

日志输出目标

控制台输出

前文已经介绍了将日志输出到控制台的配置方法。.WriteTo.Console()是一个非常简单且常用的配置,它可以将日志以文本形式输出到控制台窗口,方便在开发和调试过程中查看日志信息。

Log.Logger = new LoggerConfiguration()
   .WriteTo.Console()
   .CreateLogger();

控制台输出的日志格式默认是比较简洁的,包含日志级别、时间戳、消息等基本信息。如果需要自定义控制台输出的日志格式,可以使用Formatter参数。

Log.Logger = new LoggerConfiguration()
   .WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}")
   .CreateLogger();

在上述代码中,outputTemplate定义了日志的输出格式,{Timestamp}表示时间戳,{Level}表示日志级别,{Message}表示日志消息,{NewLine}表示换行,{Exception}表示异常信息(如果有)。

文件输出

将日志输出到文件是一个常见的需求,特别是在生产环境中。Serilog提供了Serilog.Sinks.File包来实现文件输出。首先需要通过NuGet安装该包,然后在配置中添加文件输出的设置。

using Serilog.Sinks.File;

Log.Logger = new LoggerConfiguration()
   .WriteTo.File("logs\\app.log", rollingInterval: RollingInterval.Day)
   .CreateLogger();

在上述代码中,.WriteTo.File("logs\\app.log", rollingInterval: RollingInterval.Day)表示将日志输出到logs目录下的app.log文件,并且每天滚动生成一个新的日志文件。滚动文件可以避免单个日志文件过大,方便管理和维护。rollingInterval参数可以设置为RollingInterval.Day(按天滚动)、RollingInterval.Hour(按小时滚动)、RollingInterval.Minute(按分钟滚动)等。

数据库输出

将日志记录到数据库中可以方便进行查询和分析,特别是在需要对日志数据进行复杂统计和关联查询的场景下。Serilog提供了多种数据库输出的Sink,例如Serilog.Sinks.MSSqlServer用于将日志输出到Microsoft SQL Server数据库。

首先通过NuGet安装Serilog.Sinks.MSSqlServer包。然后在数据库中创建一个表来存储日志数据,例如:

CREATE TABLE Logs (
    Id INT IDENTITY(1,1) PRIMARY KEY,
    LogLevel NVARCHAR(50),
    Message NVARCHAR(MAX),
    Exception NVARCHAR(MAX),
    Timestamp DATETIME
);

接下来在C#代码中配置Serilog将日志输出到该数据库表:

using Serilog.Sinks.MSSqlServer;

Log.Logger = new LoggerConfiguration()
   .WriteTo.MSSqlServer(
        connectionString: "your_connection_string",
        tableName: "Logs",
        autoCreateSqlTable: true)
   .CreateLogger();

在上述代码中,connectionString是数据库连接字符串,tableName指定了要写入日志的表名,autoCreateSqlTable设置为true表示如果表不存在则自动创建。

其他输出目标

除了上述常见的输出目标,Serilog还支持许多其他类型的输出,如输出到Elasticsearch、输出到Kafka、输出到Azure Blob Storage等。这些输出目标可以满足不同场景下的日志记录和处理需求,例如将日志输出到Elasticsearch可以结合Kibana进行强大的日志搜索和可视化分析。

日志模板

基本日志模板

日志模板是Serilog中用于定义日志输出格式的重要工具。前文已经介绍了在控制台输出中使用outputTemplate来定义日志格式,这就是日志模板的一种应用。

Log.Logger = new LoggerConfiguration()
   .WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}")
   .CreateLogger();

这个模板中,{Timestamp}表示日志记录的时间戳,:yyyy-MM-dd HH:mm:ss是时间戳的格式化字符串;{Level}表示日志级别;{Message}表示日志消息内容;{NewLine}表示换行符;{Exception}表示异常信息(如果有异常发生)。

自定义属性在日志模板中的使用

在结构化日志记录中,我们可以在日志模板中使用自定义的结构化属性。例如,在记录用户登录事件时,除了基本的日志信息,我们还想记录用户的角色信息。

public void UserLogin(string username, string role)
{
    var userInfo = new { Username = username, Role = role };
    Log.Information("用户 {Username} 登录,角色为 {Role}", userInfo);
}

在日志模板中,可以这样配置:

Log.Logger = new LoggerConfiguration()
   .WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] 用户 {Username} 登录,角色为 {Role}{NewLine}")
   .CreateLogger();

这样,在日志输出中就会按照我们定义的模板,清晰地显示用户登录的时间、用户名和角色信息。

日志模板的高级用法

日志模板还支持一些高级功能,比如条件输出。假设我们只想在日志级别为Error时输出异常信息,可以这样配置日志模板:

Log.Logger = new LoggerConfiguration()
   .WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{{Exception:whenRendering=({Level:u3} == 'ERROR')}}")
   .CreateLogger();

在上述模板中,{{Exception:whenRendering=({Level:u3} == 'ERROR')}}表示只有当日志级别为ERROR时才输出异常信息。这里{Level:u3}表示将日志级别转换为大写并取前三个字符进行比较。

Serilog的扩展与集成

与ASP.NET Core集成

在ASP.NET Core应用程序中使用Serilog可以方便地记录应用程序的请求、响应、异常等信息。首先在ASP.NET Core项目中通过NuGet安装Serilog.AspNetCore包。

然后在Program.cs文件中进行配置:

using Serilog;
using Serilog.Events;

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseSerilog((context, services, configuration) => configuration
   .ReadFrom.Configuration(context.Configuration)
   .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
   .Enrich.FromLogContext()
   .WriteTo.Console());

var app = builder.Build();

// 应用程序中间件和路由配置

app.Run();

在上述代码中,.UseSerilog方法将Serilog集成到ASP.NET Core应用程序中。.ReadFrom.Configuration(context.Configuration)表示从应用程序的配置文件中读取Serilog的配置;.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)Microsoft命名空间下的日志级别设置为Warning,以减少框架本身的一些不必要的日志输出;.Enrich.FromLogContext()用于丰富日志上下文信息,比如可以在日志中包含请求ID等与当前请求相关的信息。

自定义扩展

Serilog提供了丰富的扩展点,允许开发者根据自己的需求自定义日志记录行为。例如,可以创建自定义的日志Enricher来添加额外的信息到日志中。

首先定义一个自定义的Enricher类:

using Serilog.Core;
using Serilog.Events;

public class CustomEnricher : ILogEventEnricher
{
    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        var customProperty = propertyFactory.CreateProperty("CustomInfo", "这是一个自定义的信息");
        logEvent.AddPropertyIfAbsent(customProperty);
    }
}

然后在Serilog配置中使用这个自定义的Enricher:

Log.Logger = new LoggerConfiguration()
   .Enrich.With(new CustomEnricher())
   .WriteTo.Console()
   .CreateLogger();

这样,在每次记录日志时,都会自动添加一个名为CustomInfo的属性,其值为“这是一个自定义的信息”。

与其他工具集成

Serilog可以与许多其他工具和框架集成,以提供更强大的功能。例如,与ELK(Elasticsearch、Logstash、Kibana)堆栈集成,可以实现高效的日志搜索、可视化和分析。通过将日志输出到Elasticsearch,然后使用Kibana进行数据可视化展示,开发者可以快速定位和分析应用程序中的问题。

又如,与Prometheus和Grafana集成,可以将日志数据转换为监控指标,在Grafana中进行实时监控和告警。这对于从日志数据中提取关键性能指标(如请求响应时间、错误率等)并进行可视化展示非常有用。

Serilog的性能优化

批量处理与异步日志记录

在高并发的应用程序中,频繁的日志记录操作可能会对性能产生一定的影响。Serilog提供了批量处理和异步日志记录的功能来优化性能。

对于批量处理,可以使用Buffering选项。例如,在文件输出时,可以设置缓冲区大小和刷新间隔:

Log.Logger = new LoggerConfiguration()
   .WriteTo.File("logs\\app.log",
        rollingInterval: RollingInterval.Day,
        buffered: true,
        bufferSize: 100,
        flushToDiskInterval: TimeSpan.FromSeconds(5))
   .CreateLogger();

在上述代码中,buffered: true启用了缓冲区,bufferSize: 100表示缓冲区大小为100条日志记录,flushToDiskInterval: TimeSpan.FromSeconds(5)表示每5秒将缓冲区中的日志刷新到文件中。这样可以减少文件I/O操作的频率,提高性能。

异步日志记录可以通过WriteTo.Async方法实现。例如:

Log.Logger = new LoggerConfiguration()
   .WriteTo.Async(c => c.Console())
   .CreateLogger();

上述代码将控制台输出的日志记录操作设置为异步执行,这样主线程不会因为等待日志记录完成而阻塞,提高了应用程序的响应性能。

合理设置日志级别与输出目标

在生产环境中,合理设置日志级别和输出目标对于性能优化也非常重要。如果日志级别设置过低,会导致大量不必要的日志记录,增加系统开销。应该根据实际需求,将日志级别设置为合适的值,例如在生产环境中一般将日志级别设置为InformationWarning,只记录重要的信息和潜在的问题。

同时,减少不必要的输出目标也可以提高性能。如果应用程序不需要将日志输出到数据库或其他远程存储,就不要配置相应的输出目标,以避免网络传输和数据库操作带来的性能损耗。

使用日志上下文优化

在ASP.NET Core等应用程序中,使用日志上下文可以在不同的代码路径中共享一些与当前请求相关的信息,如请求ID、用户信息等。合理使用日志上下文不仅可以使日志更具可读性和关联性,还可以在一定程度上优化性能。

例如,在ASP.NET Core的中间件中,可以将请求ID添加到日志上下文:

public class RequestIdLoggingMiddleware
{
    private readonly RequestDelegate _next;

    public RequestIdLoggingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        var requestId = Guid.NewGuid().ToString();
        LogContext.PushProperty("RequestId", requestId);
        try
        {
            await _next(context);
        }
        finally
        {
            LogContext.PopProperty("RequestId");
        }
    }
}

然后在其他地方记录日志时,这个RequestId就会自动包含在日志中,方便追踪请求的整个处理流程,而且这种方式相比于每次手动传递请求ID到日志记录方法中,性能开销更小。

Serilog在不同场景下的应用

微服务架构中的应用

在微服务架构中,每个微服务都需要独立记录日志,并且需要将这些日志集中管理和分析。Serilog可以很好地满足这些需求。

每个微服务可以通过配置将日志输出到集中式的日志存储,如Elasticsearch。例如,在一个基于ASP.NET Core的微服务中,可以这样配置:

using Serilog;
using Serilog.Sinks.Elasticsearch;

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseSerilog((context, services, configuration) => configuration
   .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
   .Enrich.FromLogContext()
   .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://elasticsearch:9200"))
    {
        IndexFormat = "my - service - {0:yyyy.MM.dd}",
        AutoRegisterTemplate = true
    }));

var app = builder.Build();

// 微服务的中间件和路由配置

app.Run();

在上述代码中,.WriteTo.Elasticsearch将日志输出到Elasticsearch,IndexFormat定义了日志在Elasticsearch中的索引格式,AutoRegisterTemplate设置为true表示自动注册Elasticsearch索引模板。通过这种方式,所有微服务的日志都可以集中存储在Elasticsearch中,方便进行统一的搜索和分析。

移动应用后端服务中的应用

对于移动应用的后端服务,需要记录用户请求、业务逻辑处理、错误等信息,以便及时发现和解决问题。Serilog可以在后端服务中进行灵活配置。

例如,后端服务可能需要将日志输出到文件进行本地存储,同时将重要的错误日志发送到监控系统。可以这样配置:

Log.Logger = new LoggerConfiguration()
   .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
   .WriteTo.File("logs\\backend.log", rollingInterval: RollingInterval.Day)
   .WriteTo.Sentry(o =>
    {
        o.Dsn = "your_sentry_dsn";
        o.MinimumBreadcrumbLevel = LogEventLevel.Debug;
        o.MinimumEventLevel = LogEventLevel.Error;
    })
   .CreateLogger();

在上述代码中,.WriteTo.File将日志输出到文件,.WriteTo.Sentry将错误级别及以上的日志发送到Sentry监控系统,方便及时通知开发人员处理重要的错误。

数据处理与ETL任务中的应用

在数据处理和ETL(Extract,Transform,Load)任务中,需要记录数据抽取、转换和加载过程中的各种事件,如数据来源、数据处理结果、错误信息等。Serilog可以通过结构化日志记录这些详细信息。

例如,在一个简单的数据抽取任务中:

public void ExtractData(string sourceUrl)
{
    var dataSource = new { SourceUrl = sourceUrl };
    Log.Information("开始从 {SourceUrl} 抽取数据", dataSource);
    try
    {
        // 数据抽取逻辑
    }
    catch (Exception ex)
    {
        Log.Error(ex, "从 {SourceUrl} 抽取数据时发生错误", dataSource);
    }
}

通过这种方式,可以清晰地记录数据处理任务的执行情况,方便排查问题和进行性能优化。同时,可以根据任务的特点,将日志输出到合适的目标,如文件、数据库等。