.Net6+异常和响应中间件服务

一、为什么要有这篇文章:

        在.Net6+及以上的开发操作中,我们通常对日志记录及响应记录,还有一些其他的事物性记录需要严苛的监控或者说本系统内部的数据流向追踪,异常查找很是繁琐。后来有了日志组件Nlog,Serilog,Log4Net等日志记录组件。使用起来也很方便,但是缺少对数据流向的追踪。

        为了解决数据流向追踪,我们研究了下中间(Middleware),中间件的好处就是对代码无侵入,只用注入一次,则会全局使用,相对于拦截器来说更加方便轻量。

        本文章以 Serilog 配合中间件实现全流程的追踪,我这种菜鸟都能很快的去定位错误。之所以使用Serilog是因为它的自定义性和记录方式的多样性,很灵活。只需要配置简单的json文件就可以实现。

二、你需要准备什么?环境是什么样的?

        众所周知,在使用别人的东西前得引入别人的包,这里我们需要用到的Nuget包包含以下:
                    

# 1.Newtonsoft.Json 序列化数据参数用
# 2.Serilog.AspNetCore 这是配合.Net Core3.1及以上的Serilog主库,方便程序注入
# 3.Serilog.Sinks.File
# 4.Serilog.Expressions 
# 5.Serilog.Sinks.Console
# 6.Serilog.Sinks.Elasticsearch 等等库。。。 

读到上面这个,有的同学或者朋友就说这么多库,白瞎了,不用了。其实也完全不必苦恼。可卸库是可以被动态加载的。期望自己用什么库什么样的日志写入方式,就可以依次精简,这是Serilog比较灵活的地方,一切基于配置。跑题了,这是异常请求响应中间件的一条龙服务的。为毛要介绍这些呢?废话,查日志不需要地点?他给你生啊。

三、开始着手,你需要了解哪些?

        1.中间件是干啥的?这里就不介绍了,去看官网文档。

        2.一切基于配置和注入,我们需要一个灵活的json配置文件去配置Serilog;写一个比较好用的请求响应异常中间件。就这么两件事

        3.总结:把大象放进冰箱。冰箱的原理和大象的体积这个自己去查阅。

四、怎么做?

        1.配置Serilog的文件 2.注入Serilog及配置文件 3.写一个中间件。就这么简单。

五、看图说话

        ① 注入必要小组件(Serilog、Middleware)

        

        ② 配置json文件

        

③ 写一个中间件

using DemoApp.Exceptions; //抄的时候改成自己的dll
using Microsoft.AspNetCore.Http.Extensions;
using Newtonsoft.Json;
using System.Text;

//抄的时候改成自己的命名空间这里也是
namespace DemoApp.Middlewares
{

    /// <summary>
    /// 全局异常处理中间件
    /// </summary>
    public class ExceptionHandlingMiddleware
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly ILogger<ExceptionHandlingMiddleware> _logger;
        private readonly RequestDelegate _next;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="next"></param>
        /// <param name="logger"></param>
        public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger, IHttpContextAccessor httpContextAccessor)
        {
            _next = next;
            _logger = logger;
            _httpContextAccessor = httpContextAccessor;
        }


        public async Task Invoke(HttpContext context)
        {
            var flag = Guid.NewGuid().ToString("n");

            var request = context.Request;
            var response = context.Response;

            //确保请求body可以被重复读取
            request.EnableBuffering();

            //记录请求标记
            request.Headers["demoapp-request-flag"] = flag;

            //记录请求日志
            await LogRequest(request, flag);

            //获取Response.Body内容
            var originalBodyStream = response.Body;
            var resBodyMS = new MemoryStream();
            response.Body = resBodyMS;

            try
            {
                await _next(context);
            }
            catch (Exception e)
            {
                await HandleExceptionAsync(request, response, e, flag);
            }
            finally
            {
                //记录响应日志
                await LogResponse(response, flag);
                await resBodyMS.CopyToAsync(originalBodyStream);
                await resBodyMS.DisposeAsync();
            }

        }

        /// <summary>
        /// 处理异常信息
        /// </summary>
        /// <param name="request"></param>
        /// <param name="response"></param>
        /// <param name="e"></param>
        /// <param name="flag"></param>
        /// <returns></returns>
        private async Task HandleExceptionAsync(HttpRequest request, HttpResponse response, Exception e, string flag)
        {
            response.StatusCode = 200;

            var returnMsg = "服务开了个小差,请稍后再试";

            if (e is CustomException)
            {
                returnMsg = e.Message;
            }

            var result = new { code = 500, status = false, message = returnMsg, uuid = flag };
            var resBodyJson = JsonConvert.SerializeObject(result);

            var url = request.GetDisplayUrl();
            try
            {
                var contentType = request.ContentType?.ToLower() ?? string.Empty;
                if (contentType.StartsWith("application/json"))
                {
                    request.Body.Position = 0;
                    var stream = new StreamReader(request.Body);
                    var body = await stream.ReadToEndAsync();
                    request.Body.Position = 0;

                    _logger.LogError($"[{flag}] 系统发生异常。{Environment.NewLine}Error Message:{e.Message}{Environment.NewLine}Path:{url}{Environment.NewLine}Body:{body}{Environment.NewLine}StackTrace:{e.StackTrace}");
                }
                else
                {
                    _logger.LogError($"[{flag}] 系统发生异常。{Environment.NewLine}Error Message:{e.Message}{Environment.NewLine}Path:{url}{Environment.NewLine}StackTrace:{e.StackTrace}");
                }
            }
            catch (Exception)
            {
                _logger.LogError($"[{flag}] 系统发生异常。{Environment.NewLine}Error Message:{e.Message}{Environment.NewLine}Path:{url}{Environment.NewLine}StackTrace:{e.StackTrace}");
            }

            var resBodyByte = Encoding.Default.GetBytes(resBodyJson);
            response.ContentType = "application/json; charset=utf-8";
            await response.Body.WriteAsync(resBodyByte.AsMemory(0, resBodyByte.Length));
        }

        /// <summary>
        /// 请求日志
        /// </summary>
        /// <param name="request"></param>
        /// <param name="flag"></param>
        /// <returns></returns>
        private async Task LogRequest(HttpRequest request, string flag)
        {
            var ipaddress = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.MapToIPv4().ToString();
            var contentType = request.ContentType?.ToLower() ?? string.Empty;
            if (contentType.StartsWith("application/json"))
            {
                request.Body.Position = 0;

                var reader = new StreamReader(request.Body, Encoding.UTF8);
                var body = await reader.ReadToEndAsync();

                request.Body.Position = 0;

                _logger.LogInformation($"[{flag}] 请求内容:{ipaddress} [{request.Method}] {request.GetDisplayUrl()}{Environment.NewLine}Body:{body}");
            }
            else
            {
                _logger.LogInformation($"[{flag}] 请求内容:{ipaddress} [{request.Method}] {request.GetDisplayUrl()}");
            }
        }

        /// <summary>
        /// 响应日志
        /// </summary>
        /// <param name="response"></param>
        /// <param name="flag"></param>
        /// <returns></returns>
        private async Task LogResponse(HttpResponse response, string flag)
        {
            response.Body.Position = 0;
            var resBody = await new StreamReader(response.Body).ReadToEndAsync();
            //重置请求体流的位置,以便下一个中间件可以读取它
            response.Body.Position = 0;

            _logger.LogInformation($"[{flag}] 响应内容:{response.StatusCode}{Environment.NewLine}Body:{resBody}");
        }
    }

    /// <summary>
    /// 中间件注入方法
    /// </summary>
    public static class ExceptionHandlingExtensions
    {
        public static IApplicationBuilder UseExceptionHandling(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<ExceptionHandlingMiddleware>();
        }
    }
}

以上基本上完成了请求响应异常中间件一条龙服务。 Serilog 的json配置可以去他们官网看我认为比较方便的方式还是写入es和文本文件。ES本地化部署了可以减少很多运维上的毛病

六、最兴奋的时候到了,看看...结果

        为什么说是一条龙服务请看图,返回结果的uuid可以看到它的到生命周期,从一而终,

中间还有自己的写的日志啥的都可以。uuid :2d195e0b6a824fc58fd566b44253433e

控制台日志        

文本日志(请求和响应日志)

                                                        文本日志(异常或者错误日志)

                                                                

以上是所有日志记录,我还是觉得es更方便,这样就可以一步到位看到所有的生命周期了。

代码里面有私藏,可能少了个类,是个自定义的异常

                                               自定义异常类,写义务异常也可以这样做

   /// <summary>
   /// 自定义业务异常
   /// </summary>
   public class CustomException : Exception
   {
       public CustomException(string message) : base(message)
       {
       }

       public CustomException(string message, Exception ex) : base(message, ex)
       {

       }
   }

结束,很多废话。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值