GRpc異常處理Filter

全局錯誤處理服務端

微軟已經實施了Interceptors,它們類似於FilterMiddlewares在ASP.NET MVC的核心或的WebAPI,它們可以用於全局異常處理,日誌記錄,驗證等。
這是伺服器端Interceptor自己的實現,Continuation是必須等待的Task,然後,如果引發了任何異常,則可以根據所獲得的異常來控制RpcException和關聯的StatusCode

using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Logging;
using System;
using System.Data.SqlClient;
using System.Threading.Tasks;

namespace DemoGrpc.Web.Logging
{
    public class LoggerInterceptor : Interceptor
    {
        private readonly ILogger<LoggerInterceptor> _logger;

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

        public async override Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
            TRequest request,
            ServerCallContext context,
            UnaryServerMethod<TRequest, TResponse> continuation)
        {
            LogCall(context);
            try
            {
                return await continuation(request, context);
            }
            catch (SqlException e)
            {
                _logger.LogError(e, $"An SQL error occured when calling {context.Method}");
                Status status;

                if (e.Number == -2)
                {
                    status = new Status(StatusCode.DeadlineExceeded, "SQL timeout");
                }
                else
                {
                    status = new Status(StatusCode.Internal, "SQL error");
                }
                throw new RpcException(status);
            }
            catch (Exception e)
            {
                _logger.LogError(e, $"An error occured when calling {context.Method}");
                throw new RpcException(Status.DefaultCancelled, e.Message);
            }
            
        }

        private void LogCall(ServerCallContext context)
        {
            var httpContext = context.GetHttpContext();
            _logger.LogDebug($"Starting call. Request: {httpContext.Request.Path}");
        }
    }
}

註冊方式如下

using DemoGrpc.Web.Logging;
using DemoGrpc.Web.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace DemoGrpc.Web
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            //註冊GRpc全局異常捕獲
            services.AddGrpc(options =>
            {
                options.Interceptors.Add<LoggerInterceptor>();
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<CountryGrpcService>();

                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: //go.microsoft.com/fwlink/?linkid=2086909");
                });
            });
        }
    }
}

第二種方法也可以捕獲到GRpc異常,但是寫法比較粗糙。不推薦使用

using AutoMapper;
using DemoGrpc.Domain.Entities;
using DemoGrpc.Protobufs;
using DempGrpc.Services.Interfaces;
using Grpc.Core;
using System;
using System.Threading.Tasks;

public class CountryGrpcService : CountryService.CountryServiceBase
{
    private readonly ICountryService _countryService;
    private readonly IMapper _mapper;

    public CountryGrpcService(ICountryService countryService, IMapper mapper)
    {
        _countryService = countryService;
        _mapper = mapper;
    }

    public override async Task<CountriesReply> GetAll(EmptyRequest request, ServerCallContext context)
    {
        try
        {
            var countries = await _countryService.GetAsync();
            return _mapper.Map<CountriesReply>(countries);
        }
        catch (Exception e)
        {
            throw new RpcException(Status.DefaultCancelled, e.Message);
        }
    }
}

Rpc異常資訊介紹如下

一個普通標題 一個普通標題
Aborted 操作被中止,通常是由於並發性問題,如順序器檢查失敗、事務中止等。
AlreadyExists 試圖創建的一些實體(例如,文件或目錄)已經存在。
Cancelled 該操作被取消(通常由調用者取消)。
DataLoss 不可恢復的數據丟失或損壞。
DeadlineExceeded 操作完成前截止日期已過期。對於改變系統狀態的操作,即使操作已經成功完成,也會返回此錯誤。例如,來自伺服器的成功響應可能會延遲到截止日期過期。
FailedPrecondition 操作被拒絕,因為系統沒有處於執行操作所需的狀態。例如,要刪除的目錄可能是非空的,一個rmdir操作被應用到一個非目錄,等等。
Internal 內部錯誤。表示底層系統期望的某些不變數被打破。
InvalidArgument 客戶端指定了無效的參數。注意,這與FAILED_PRECONDITION不同。INVALID_ARGUMENT表示與系統狀態無關的參數(例如格式不正確的文件名)。
NotFound 一些被請求的實體(例如,文件或目錄)沒有找到。
OK 成功返回
OutOfRange 操作嘗試超過有效範圍。例如,查找或讀取文件的前端。
PermissionDenied 調用者沒有許可權執行指定的操作。PERMISSION_DENIED不能用於由於耗盡某些資源而導致的拒絕(對於那些錯誤,應該使用RESOURCE_EXHAUSTED)。如果無法識別調用者,則不能使用PERMISSION_DENIED(對於那些錯誤,則使用UNAUTHENTICATED)。
ResourceExhausted 某些資源已經耗盡,可能是每個用戶的配額,或者可能是整個文件系統空間不足。
Unauthenticated 未認證/授權
Unavailable 該服務目前不可用。這很可能是一種暫時的情況,可以通過後退重新嘗試來糾正。注意,重試非冪等操作並不總是安全的。
Unimplemented 此服務中未實現或不支援/啟用操作。
Unknown 未知的錯誤。可能返回此錯誤的一個示例是,如果從另一個地址空間接收到的狀態值屬於此地址空間中未知的錯誤空間。如果api沒有返回足夠的錯誤資訊,則可能會將其轉換為此錯誤。

具體地址://grpc.github.io/grpc/csharp/api/Grpc.Core.StatusCode.html

RpcException有相對應的重載:具體如下,可以自定義異常返回的資訊

全局錯誤處理客戶端

客戶端也可以通過攔截器處理錯誤(實現客戶端事件,如AsyncUnaryCall):

using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

namespace ConsoleAppGRPC.Logging
{
    public class LoggerInterceptor : Interceptor
    {
        private readonly ILogger<LoggerInterceptor> _logger;

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

        public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
            TRequest request,
            ClientInterceptorContext<TRequest, TResponse> context,
            AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
        {
            LogCall(context.Method);

            var call = continuation(request, context);

            return new AsyncUnaryCall<TResponse>(HandleResponse(call.ResponseAsync), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose);
        }

        private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> t)
        {
            try
            {
                var response = await t;
                _logger.LogDebug($"Response received: {response}");
                return response;
            }
            catch (RpcException ex)
            {
                _logger.LogError($"Call error: {ex.Message}");
                return default;
            }
        }

        private void LogCall<TRequest, TResponse>(Method<TRequest, TResponse> method) where TRequest : class where TResponse : class
        {
            _logger.LogDebug($"Starting call. Type: {method.Type}. Request: {typeof(TRequest)}. Response: {typeof(TResponse)}");
        }
    }
}

使用方法:

using DemoGrpc.Domain.Entities;
using DemoGrpc.Protobufs;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;
using static DemoGrpc.Protobufs.CountryService;
using Microsoft.Extensions.Logging;
using ConsoleAppGRPC.Logging;

namespace ConsoleAppGRPC
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var services = new ServiceCollection();
            services.AddScoped<LoggerInterceptor>();
            services.AddLogging(logging =>
            {
                logging.AddConsole();
                logging.SetMinimumLevel(LogLevel.Debug);
            });

            services.AddGrpcClient<CountryServiceClient>(o =>
            {
                o.Address = new Uri("//localhost:5001");
            }).AddInterceptor<LoggerInterceptor>();

            var provider = services.BuildServiceProvider();
            var client = provider.GetRequiredService<CountryServiceClient>();
            var logger = provider.GetRequiredService<ILogger<Program>>();

            var countries = (await client.GetAllAsync(new EmptyRequest()))?.Countries.Select(x => new Country
            {
                CountryId = x.Id,
                Description = x.Description,
                CountryName = x.Name
            }).ToList();

            if (countries != null)
            {
                logger.LogInformation("Found countries");
                countries.ForEach(x => Console.WriteLine($"Found country {x.CountryName} ({x.CountryId}) {x.Description}"));
            }
            else
            {
                logger.LogDebug("No countries found");
            }
        }
    }
}

得到的結果資訊

結論
我們在本文中看到了如何全局處理錯誤。攔截器、RpcException、狀態程式碼和返回資訊的使用為我們提供了一定的靈活性,比如訂製錯誤和向客戶端發送相關錯誤的可能性。🙂

如果大家想要了解更多的Interceptors,請給我留言。

如有哪裡講得不是很明白或是有錯誤,歡迎指正
如您喜歡的話不妨點個贊收藏一下吧