asp.net core日志与异常处理小结

news/2024/9/28 17:33:48/

asp.net core的webApplicationBuilder中自带了一个日志组件,无需手动注册服务就能直接在控制器中构造注入,本文主要介绍了net core日志与异常处理小结,需要的朋友可以参考下

ILogger简单使用

asp.net corewebApplicationBuilder中自带了一个日志组件。无需手动注册服务就能直接在控制器中构造注入。

public HomeController(ILogger<HomeController> logger)
{_logger = logger;
}
_logger.LogTrace("trace{path}", HttpContext.Request.Path);
_logger.LogDebug("debug{path}", HttpContext.Request.Path);
_logger.LogInformation("info{path}", HttpContext.Request.Path);
_logger.LogWarning("warn{path}", HttpContext.Request.Path);
_logger.LogError("error{path}", HttpContext.Request.Path);
_logger.LogCritical("critical{path}", HttpContext.Request.Path);
_logger.Log(LogLevel.Information, "Log{path}", HttpContext.Request.Path);

打印效果如下

 

在一个日志输出中,日志的第一行输出是日志类别,来自泛型参数<HomeController>的完全限定类名。第二行是模板字符串输出,可以看到从info级别开始才有输出,这是因为在appsetting文件中配置了LogLevel为Information,过滤掉了该等级之下的输出。

MSDN推荐在方法中多次详细的打印日志,但打印方法使用TraceInformation。开发时将日志等级设置为Warn,避免过多信息显示。在排查故障时才将日志等级设置为Trace

打印异常信息

try
{throw new Exception("自定义异常");
}
catch (Exception e)
{_logger.LogTrace("trace{path}", e);_logger.LogDebug("debug{path}", e);_logger.LogInformation("info{path}", e);_logger.LogWarning("warn{path}", e);_logger.LogError("error{path}", e);_logger.LogCritical("critical{path}", e);_logger.Log(LogLevel.Information, "Log{path}", e.ToString());
}

打印效果如下

 

日志的第一行是泛型参数,第二行开始是字符串模板。其中有参数e的异常类型,异常信息e.Message。第三行开始时异常的调用堆栈e.StackTrace最后一个打印中调用了e.ToString(),结果是一样的。由此可见日志打印时,碰到了非字符串参数,就会调用其ToString()方法。那么我们可以通过重写这个方法来改变日志输出吗?

重写ToString

public class Person
{public int Id { get; set; }public string Name { get; set; }public int Age { get; set; }public override string ToString(){return JsonSerializer.Serialize(this);}
}
_logger.LogError("{path}", new Person() { Id=1,Name="张三",Age=19});
_logger.LogCritical("{path}", new Person() { Id = 2, Name = "王浩", Age = 20 });

这里我在重写中调用了序列化方法,打印结果是一个json数据

 

日志配置方式

配置日志文件介质,有三种方法,都是ILoggingBilder调用AddConsole。他们差不多,但有微小差别。第一种是配置容器中的日志服务,后面两种是配置服务主机上的日志。

主机生成器会初始化默认配置,然后在生成主机时将已配置的 ILoggerFactory 对象添加到主机的 DI 容器

所以效果是一样的

 

//第一种,配置应用程序级别的日志记录器
builder.Services.AddLogging(configure =>
{configure.AddConsole();
});
//第二种,配置应用程序主机级别的日志记录器。和第三种一样,但不推荐使用这个。
builder.Host.ConfigureLogging(configure =>
{configure.AddConsole();
});
//第三种,配置应用程序主机级别的日志记录器
builder.Logging.AddConsole();

Log等级

asp.net core的日志默认扩展中,定义LogTrace LogDebug LogInformation LogWarning LogError LogCritical Log 7种常用方法。对应的日志有7个级别

// 定义日志严重性级别。
public enum LogLevel
{//包含最详细消息的日志。这些消息可能包含敏感的应用程序数据//默认情况下禁用这些消息,生产环境中永远不应启用Trace = 0,//用于开发过程中的交互式调查。这些日志主要包含调试有用的信息,没有长期价值Debug = 1,//跟踪应用程序的一般流程。这些日志应具有长期价值Information = 2,//强调应用程序流程中的异常或意外事件,但不会导致应用程序执行停止Warning = 3,//强调由于失败导致当前执行流程停止的日志。这些日志应指示当前活动中的失败,而不是应用程序范围的失败Error = 4,//描述不可恢复的应用程序或系统崩溃,或者需要立即关注的灾难性失败Critical = 5,//不用于编写日志消息。指定日志类别不应写入任何消息None = 6,
}

异常

在开发环境,builder默认会为应用配置这个异常中间件app.UseDeveloperExceptionPage(),这个配置是写在源码中的,所以不需要我们显式配置。如果我们不想显示这个页面,那可以配置中间件UseExceptionHandler。这个中间件要配置在UseDeveloperExceptionPage后面,因为中间件调用是递归的,异常发生在终结点中间件中,之后就是请求处理管道递归跳出阶段,所以异常捕获也在这个阶段。调用顺序和配置顺序是相反的。

或者根据环境app.Environment.IsDevelopment()来判断使用是否使用UseExceptionHandler。至于UseDeveloperExceptionPage则不用担心,只要不是显示配置,开发环境中不会进入这个中间件。不知道是由于条件编译,还是做了环境判断。

还还有一种方法是在这两个中间件后面自定义中间件捕获异常,短路管道。

我注意到,不管使用UseDeveloperExceptionPage还是UseExceptionHandler中间件,控制台中都会出现异常日志,这是为什么?这是因为这两个中间件中都调用了日志打印。所以我们可以自己实现一个异常处理中间件,这样就不会打印日志了。

app.UseDeveloperExceptionPage();
app.UseExceptionHandler("/Home/Error");
app.Use(async (context, next) =>
{try{await next(context);}catch (Exception e){await context.Response.WriteAsync("Custom Exception MiddleWare");}
});

碰到异常打印日志是正常操作,所以这不需要我们去关心。但是,在前面的截图中,UseExceptionHandler中间件的异常打印的问题在于,没有打印请求路径、请求参数等信息。我们只知道哪里出错了,却不知道是什么原因引起的,怎么复现,这是很不完善的。不知道当初为什么这样设计。所以最好是自己重新写个异常处理中间件,设置响应内容以及控制日志打印。

自定义异常中间件打印请求和异常日志

public class ExceptionMiddleware
{private readonly RequestDelegate next;public ExceptionMiddleware(RequestDelegate next){this.next = next;}public async Task InvokeAsync(HttpContext context){try{await next(context);}catch (Exception e){var _logger=context.RequestServices.GetService<ILogger<ExceptionMiddleware>>();var bodystring="";if (context.Request.ContentType==null || !context.Request.ContentType.Contains("multipart/form-data")){using (StreamReader reader = new StreamReader(context.Request.Body, Encoding.UTF8)){bodystring = await reader.ReadToEndAsync();}}else{//不包含文件实际内容foreach (var formField in context.Request.Form){bodystring += formField.Key + " " + formField.Value+",";}}_logger.LogError("堆栈:{e}\n\t路径:{c}\n\t查询字符串:{p}\n\t内容:{f}", e,context.Request.Path,context.Request.QueryString,bodystring);await context.Response.WriteAsync("Custom Exception MiddleWare");}}
}

使用Serilog输出日志到文件介质

由于asp.net core自己没有提供文件介质的提供程序,所以我转到使用Serilog。仅仅注册了一个服务,其他的一切不变。
引入包Serilog.AspNetCore。并不需要引入Serilog.Sinks.File,因为Serilog.AspNetCore已经引入了文件介质的提供程序

日志管线配置Serilog提供程序

日志系统包括如下几个概念:日志管线提供程序介质。严格来说介质不算。
日志系统又叫日志管线logging pipeline。日志管线中的每个提供程序都会处理一次日志,并输出到自己的介质中。

  • 配置serilog子管线

 

Log.Logger = new LoggerConfiguration()//最小输出等级.MinimumLevel.Warning()//增加介质.WriteTo.Console().WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day).CreateLogger();
//注册serilog服务
builder.Services.AddSerilog();

配置完整的日志管线这里默认的控制台介质得到了两个提供程序,一个是默认的AddConsole,一个是Serilog的WriteTo.Console。按照管线中配置的顺序,这两个提供程序都会向控制台打印一次。而Serilog还向管线中添加了一个到文件介质的提供程序,所以还会产生一个日志文件。

builder.Services.AddLogging(logbuilder =>
{logbuilder//删除管线上默认的提供程序.ClearProviders()//serilog子管线上的提供程序合并进来.AddSerilog().AddConsole();
});

控制台介质两个提供程序均处理了日志打印

  • 文件介质

输出模板

回想默认日志提供程序和serilog日志提供程序,都会在我们的消息之外附加信息,比如等级、类名。这是怎么配置的?

  • 默认日志提供程序的格式是 [LogLevel]: [EventId] [Category] Message。可以通过继承ConsoleFormatter改写模板。但是很麻烦。而且颜色,字体还不知道怎么控制。
public class CustomConsoleFormatter : ConsoleFormatter
{private readonly string _formatTemplate;public CustomConsoleFormatter(IOptions<TemplatedConsoleFormatterOptions> options) : base(name: "Custom"){_formatTemplate = options.Value.Template ?? "[{Timestamp:HH:mm:ss} {Level}] [{EventId}] {Message}{NewLine}{Exception}";}public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider scopeProvider, TextWriter textWriter){var timestamp = DateTime.UtcNow;var level = logEntry.LogLevel.ToString();var eventId = logEntry.EventId.Id;var message = logEntry.Formatter(logEntry.State, logEntry.Exception);var exception = logEntry.Exception != null ? logEntry.Exception.ToString() : "";var newLine = Environment.NewLine;var logOutput = ReplaceTemplateValues(_formatTemplate, timestamp, level, eventId, message, exception, newLine);textWriter.Write(logOutput);}private string ReplaceTemplateValues(string template, DateTime timestamp, string level, int eventId, string message, string exception, string newLine){return Regex.Replace(template, @"{(\w+)(?::([^}]+))?}", match =>{var key = match.Groups[1].Value;var format = match.Groups[2].Value;switch (key){case "Timestamp":return timestamp.ToString(format);case "Level":return level;case "EventId":return eventId.ToString();case "Message":return message;case "NewLine":return newLine;case "Exception":return exception;default:return match.Value;}});}
}
public class TemplatedConsoleFormatterOptions : ConsoleFormatterOptions
{public string? Template { get; set; }
}
//使用模板
builder.Services.AddLogging(logbuilder =>
{logbuilder.ClearProviders()//.AddSerilog().AddConsole(option =>{//使用名为Custom的模板配置option.FormatterName = "Custom";})//添加一个控制台模板配置.AddConsoleFormatter<CustomConsoleFormatter, TemplatedConsoleFormatterOptions>(foption =>{foption.Template = "[{Timestamp:HH:mm:ss} {Level}] [{EventId}] {Message:lj}{NewLine}{Exception}";});
});

 

 

Serilog输出模板相比之下Serilog就简单得多,直接指定模板就行 

Log.Logger = new LoggerConfiguration()//最小输出等级.MinimumLevel.Warning()//增加介质.WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}").WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}").CreateLogger();

Serilog还能向模板中增加字段,使用Enrichers

Log.Logger = new LoggerConfiguraition()  .Enrich.FromLogContext().Enrich.WithProperty("Application", "Demo").WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{Properties:j}{NewLine}{Exception}")
// 设置 ThreadContext 或 CallContext 的属性
ThreadContext.Properties["UserId"] = 42;
CallContext.LogicalSetData("UserName", "John Doe");
Log.Information("User {UserId} logged in as {UserName}", ThreadContext.Properties["UserId"], CallContext.LogicalGetData("UserName"));

 


http://www.ppmy.cn/news/1531286.html

相关文章

4、FPGA特征简介

1、FPGA器件简介 FPGA是由存放在片内的RAM来设置其工作状态的&#xff0c;因此工作时需要对片内RAM进行编程。用户可根据不同的配置模式&#xff0c;采用不同的编程方式。FPGA有如下几种配置模式。 1&#xff09;并行模式&#xff1a;一片EPROM配置一片FPGA。 2&#xff09;主从…

Python知识点:如何使用Python进行卫星数据分析

开篇&#xff0c;先说一个好消息&#xff0c;截止到2025年1月1日前&#xff0c;翻到文末找到我&#xff0c;赠送定制版的开题报告和任务书&#xff0c;先到先得&#xff01;过期不候&#xff01; 如何使用Python进行卫星数据分析 卫星数据分析是地球观测领域的一项关键技术&a…

【Python-tkinter】实现简单的文本编辑器(附带教程源码)

如果你也是刚入门的小伙伴呢&#xff0c;小编为你们准备了入门Python学习籽料和Python入门实践&#xff0c;点击领取&#xff08;无偿获得&#xff09; 利用tkinter实现简单的文本编辑器。创建一个简单的文本编辑器。可以用读文件的方式在一个文本域里显示一些文字供用户编辑…

【STM32系统】基于STM32设计的智能垃圾桶(语音、颜色识别、称重、光强、烟雾、人体识别、步进电机、水泵)——文末资料下载

基于STM32设计的智能垃圾桶 演示视频: 基于STM32设计的智能垃圾桶 功能简介: 四个按键可分别打开四个垃圾桶(可回收垃圾、厨余垃圾、有害垃圾、其他垃圾) oled显示屏显示四个垃圾桶的打开/关闭状态、烟雾浓度、光照强度、称重的重量和识别到的颜色(白色、红色、绿色、蓝…

基于C+++Mysql实现(CS界面)图书管理系统

图书管理系统 实验内容、步骤以及结果 做出数据流图和数据字典。 在数据流图和字典的基础上做出 E-R 图(概念结构设计)。 学生&#xff1a; 图书&#xff1a; 管理员&#xff1a; 汇总&#xff1a; 在 E-R 图基础上进行关系模式设计&#xff08;至少满足 3NF&#xff09;&am…

【专题】2024年中国白酒行业数字化转型研究报告合集PDF分享(附原数据表)

原文链接&#xff1a;https://tecdat.cn/?p37755 消费人群趋于年轻化&#xff0c;消费需求迈向健康化&#xff0c;消费场景与渠道走向多元化&#xff0c;这些因素共同驱动企业凭借数据能力来适应市场的变化。从消费市场来看&#xff0c;消费群体、需求、场景及渠道皆展现出与…

基于MaxScale搭建MariaDB读写分离集群的方法【2024年最新版】

1、什么是MaxScale MaxScale是MariaDB数据库的一个中间件&#xff0c;为MariaDB提供代理服务&#xff0c;主要可以实现读写分离和一定的负载均衡功能&#xff0c;其中读写分离可将读操作和写操作分离到不同的数据库服务器上&#xff0c;以提高系统的整体性能和扩展性&#xff…

RHCSA认证-Linux(RHel9)-Linux入门

文章目录 概要一、创建、查看和编辑⽂本1.1 输出重定向1.2 vim编辑器1.3 shell 变量1.5 获取帮助 二、管理本地用户和组2.1 描述用户2.2 切换用户和赋权2.3 用户管理2.4 用户组管理2.5 密码策略 三、控制文件访问3.1 列出文件和文件权限3.2 更改文件权限和拥有者3.3 控制默认权…