Skip to main content

使用日志

分类:  Asp.net Core入门 标签:  #Asp.Net core基础 #基础 #Web 发布于: 2023-06-04 20:31:11

Asp.net core中除了Console以外,其他的日志提供者都存储日志,例如Azure Application Insights将日志存储到该服务中,框架已经向用户提供了不少日志提供者,我们还是从最基本的模板来看一下默认已经启用了哪些日志提供者。

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

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

我们需要注意的是方法CreateDefaultBuilder, 在这个方法里,完成了:

  • 创建Generic Host
  • 调用方法CreateDefaultBuilder:
    • Console
    • Debug
    • EventSource
    • EventLog: (Windows Only)

以上是默认的日志提供者,如果用户需要自己定制日志,那么可以使用如下的方法:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureLogging(logging =>
        {
            logging.ClearProviders();
            logging.AddConsole();
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

注意扩展方法ConfigureLogging

默认配置和基本用法

我们来看一下日志提供者的基本用法,DI在整个框架中都是需要用到的,因此日志也不例外,为了创建日志,我们需要从DI里通过构造函数注入:ILoger<TCategoryName>, 这里有一个重要的概念就是日志的分类(Category), 分类是一个字符串,类似于一个域名的层级形式,除了在日志里记录这个Category之外,我们还在配置文件里使用该分类,来定义某个分类的日志等级,以过滤日志。

从构造函数里注入,并使用:

public class AboutModel : PageModel
{
    private readonly ILogger _logger;

    public AboutModel(ILogger<AboutModel> logger)
    {
        _logger = logger;
    }
    public string Message { get; set; }

    public void OnGet()
    {
        Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
        _logger.LogInformation(Message);
    }
}

这里有两个非常重要的概念,就是日志的等级,以及日志的分类,关于分类我们上面已经学习过了。

日志配置

我们使用配置Logging来配置日志,同时这个项下面还有很多个子项,例如:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

从这个配置上可以看出:

  • DefaultMicrosoftMicrosoft.Hosting.Lifetime 都是表示日志的分类。
  • 分类Microsoft 分类应用所有以Microsoft开头的分类。
  • 同时每个分类的日志等级可以定位不一样的。
  • 缺省的还是由default来定义。
  • 如果某一个提供者没有指定相应的配置,那么就会使用Default

再看一个例子:

{
  "Logging": {
    "LogLevel": { // All providers, LogLevel applies to all the enabled providers.
      "Default": "Error", // Default logging, Error and higher.
      "Microsoft": "Warning" // All Microsoft* categories, Warning and higher.
    },
    "Debug": { // Debug provider.
      "LogLevel": {
        "Default": "Information", // Overrides preceding LogLevel:Default setting.
        "Microsoft.Hosting": "Trace" // Debug:Microsoft.Hosting category.
      }
    },
    "EventSource": { // EventSource provider
      "LogLevel": {
        "Default": "Warning" // All categories of EventSource provider.
      }
    }
  }
}

这里的DebugEventSource就是专门针对日志提供者设定的配置。

上述也可以以这种形式指定:Logging:Default:LogLevel:Default:Information

也可以通过环境变量,例如: set Logging__LogLevel__Microsoft=Information

简单的描述一下日志过滤的规则算法:

  • 使用提供者或者提供者的别名,选择所有符合条件的日志,如果没有符合的,则选择空提供者。
  • 从上一步的结果中,选择符合指定分类名的日志,如果没有符合的分类,则选择所有没有指定分类的日志
  • 如果由多条日志被选择,最少用一条
  • 如果全部没选中,则使用MinimumLevel

Log Category

日志分类主要的作用是用于日志的过滤,例如在配置里设定某个分类的等级,在日志显示的时候也显示出具体的分类,按照约定一般情况推荐使用类名来表示分类。

基本的用法可以使用泛型版的ILogger: ILogger

public class PrivacyModel : PageModel
{
    private readonly ILogger<PrivacyModel> _logger;

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

    public void OnGet()
    {
        _logger.LogInformation("GET Pages.PrivacyModel called.");
    }
}

也可以明确的指定分类名称,这个时候需要使用方法ILoggerFactory.CreateLogger:

public class ContactModel : PageModel
{
    private readonly ILogger _logger;

    public ContactModel(ILoggerFactory logger)
    {
        _logger = logger.CreateLogger("MyCategory");
    }

    public void OnGet()
    {
        _logger.LogInformation("GET Pages.ContactModel called.");
    }

Log Level

日志等级这个很好理解,一般情况分成这几类:

  • Trace: 0
  • Debug: 1
  • Information: 2
  • Warning: 3
  • Error: 4
  • Critical: 5
  • None: 6

日志级别从小到大,显示的信息也会越来越窄,Trace(0), 显示最多的信息。注意日志的方法LogTrace, 类似这样的。

Log Event Id

每个日志都可以指定一个事件的ID, 这里的ID是用户自行定义的,非常方便用自己的系统中,用于快速的判断问题。

public class MyLogEvents
{
    public const int GenerateItems = 1000;
    public const int ListItems     = 1001;
    public const int GetItem       = 1002;
    public const int InsertItem    = 1003;
    public const int UpdateItem    = 1004;
    public const int DeleteItem    = 1005;

    public const int TestItem      = 3000;

    public const int GetItemNotFound    = 4000;
    public const int UpdateItemNotFound = 4001;
}

使用事件ID

[HttpGet("{id}")]
public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
{
    _logger.LogInformation(MyLogEvents.GetItem, "Getting item {Id}", id);

    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null)
    {
        _logger.LogWarning(MyLogEvents.GetItemNotFound, "Get({Id}) NOT FOUND", id);
        return NotFound();
    }

    return ItemToDTO(todoItem);
}

可以看到日志显示的方法是: LogInformation('事件ID', '消息模板', '替换变量')

Log Message Template

日志消息中使用命名模板,如下代码所示:

[HttpGet("{id}")]
public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
{
    _logger.LogInformation(MyLogEvents.GetItem, "Getting item {Id}", id);

    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null)
    {
        _logger.LogWarning(MyLogEvents.GetItemNotFound, "Get({Id}) NOT FOUND", id);
        return NotFound();
    }

    return ItemToDTO(todoItem);
}

string apples = 1;
string pears = 2;
string bananas = 3;

_logger.LogInformation("Parameters: {pears}, {bananas}, {apples}", apples, pears, bananas);

Log Exception

日志的方法有重载可以直接显示或者存储异常的,如下代码所示:

[HttpGet("{id}")]
public IActionResult TestExp(int id)
{
    var routeInfo = ControllerContext.ToCtxString(id);
    _logger.LogInformation(MyLogEvents.TestItem, routeInfo);

    try
    {
        if (id == 3)
        {
            throw new Exception("Test exception");
        }
    }
    catch (Exception ex)
    {
        _logger.LogWarning(MyLogEvents.GetItemNotFound, ex, "TestExp({Id})", id);
        return NotFound();
    }

    return ControllerContext.MyDisplayRouteInfo();
}

Log Scope

Log Scope可以将一组逻辑的操作组织到一起,这个逻辑组可以附加相同的数据到每个日志上,例如:

[HttpGet("{id}")]
public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
{
    TodoItem todoItem;

    using (_logger.BeginScope("using block message"))
    {
        _logger.LogInformation(MyLogEvents.GetItem, "Getting item {Id}", id);

        todoItem = await _context.TodoItems.FindAsync(id);

        if (todoItem == null)
        {
            _logger.LogWarning(MyLogEvents.GetItemNotFound, 
                "Get({Id}) NOT FOUND", id);
            return NotFound();
        }
    }

    return ItemToDTO(todoItem);
}

Service中使用Log

在Service中使用构造函数注入日志的实例,可以直接使用的。

高性能日志处理

这个部分的原因我们之前有一个一篇介绍过,具体的做法实际上是用LoggerMessage.Define来定义,例如:

先定义一个委托:

private static readonly Action<ILogger, Exception> _indexPageRequested;

然后将这个委托指向LoggerMessage.Define指定的委托:

_indexPageRequested = LoggerMessage.Define(
    LogLevel.Information, 
    new EventId(1, nameof(IndexPageRequested)), 
    "GET request for Index page");

最后使用一个扩展方法来准备调用它:

public static void IndexPageRequested(this ILogger logger)
{
    _indexPageRequested(logger, null);
}

在日常使用中使用该扩展方法:

public async Task OnGetAsync()
{
    _logger.IndexPageRequested();

    Quotes = await _db.Quotes.AsNoTracking().ToListAsync();
}

日志这个部分可以先介绍到这里了。