深入浅出Blazor webassembly之Logging

本文详细介绍了ASP.NET Core应用程序中日志配置的方法,包括在appsettings.json中设置日志级别,以及针对Blazor WebAssembly应用的特殊性。文章还比较了不同日志实现的优缺点,如内置的Logging类和Blazor.Extensions.Logging扩展。此外,还探讨了Serilog.Sinks.BrowserConsole的使用,提供了详细的配置和示例代码。最后,推荐使用Serilog.Sinks.BrowserConsole作为日志解决方案。

===============================

Aspnet core 的日志配置文件设置

===============================

appsettings.json 文件中可以设置日志

   "Logging": {
        "LogLevel": {  //No specific provider, 将适用于所有的Provider
            "Default": "Information", //Default category for Logger Object, 即 ILogger<T> 中 T 为 category
            "Microsoft": "Warning", 
            "Microsoft.Hosting.Lifetimne": "Information"
        },
        "EventSource": {  //仅适用于 EventSource Provider
            "Default": "Information"
        }
    }

logging配置的层次关系是  Logging.{provider_name.}LogLevel

对于 blazor wsam 应用,  配置文件的logging 配置并不起任何作用. 

但可以在 program.cs Main函数用代码的形式设置最低输出级别.

 builder.Logging.SetMinimumLevel(LogLevel.Information);

===============================

最简单的日志输出

===============================

在Blazor wasm 程序中使用Console.WriteLine(), 即可输出到浏览器的console 中, 非常方便. 

缺点是: 没有LogLevel的控制

razor 文件中的代码:

    private void IncrementCount()
    {
        currentCount++;
        Console.WriteLine("Console.WriteLine() output. ");
    }

效果:

===============================

内置的Logging类

===============================

Blazor项目中, 在默认情况下, 即可注入 Microsoft.Extensions.Logging.Logger 日志类实例.

优点: 使用非常方便, 而且可以进行 LogLevel 控制

缺点: 仅仅能输出字符串信息, 不能直接输出 object 对象或List或数组.

razor 文件代码:

@page "/counter"
@using Microsoft.Extensions.Logging
@inject ILogger<Counter> MyLogger 


<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
        MyLogger.LogInformation("LogInformation");
        MyLogger.LogTrace("LogTrace");  //内置Logger类 不支持这个Trace level
        MyLogger.LogWarning("LogWarning");
        MyLogger.LogDebug("LogDebug");
        MyLogger.LogError("LogError");
         
        MyLogger.LogCritical("LogCritical");  //输出这个记录的日志, 浏览器底部将出现一个Unhandled error 提示条
    }
}

效果如下:

===============================

增强版的 Blazor.Extensions.Logging 包

===============================

增强版的 Blazor.Extensions.Logging 包具体增强点有:

1. 能直接输出接输出object/list/array 对象

2. 支持Trace Level的日志, 不过这个用处并不大

3. logging输出格式也比较好看.

该组件的问题是:  在代码中一起输出两行 Logging , 浏览器中看到的次序正好和代码相反.

Blazor.Extensions.Logging 同样实现了 ILogger<T> 接口,  所以使用方式和内置的Logger类一样. 

使用步骤:

1. 为项目增加: Blazor.Extensions.Logging 包引用

dotnet add package Blazor.Extensions.Logging

2. 将 扩展logging 类注册的 DI 容器, Program.cs 的 Main() 方法中,

using Blazor.Extensions.Logging;

//Main 方法中
// Add Blazor.Extensions.Logging.BrowserConsoleLogger
builder.Services.AddLogging(builder => builder
    .AddBrowserConsole()
    .SetMinimumLevel(LogLevel.Trace)
);

3. 在Razor 类中, 注入 ILogger<T> 对象

@* //引入 ILogger 接口 *@
@using Microsoft.Extensions.Logging    
@* //引入 Log(object) 扩展方法 *@
@using Blazor.Extensions.Logging   
@* //注入 Logger 对象 *@
@inject ILogger<Counter> MyLogger    
@code {

    private void IncrementCount()
    {
        currentCount++;
        MyLogger.LogInformation(MyLogger.GetType().ToString());
        MyLogger.LogInformation("1: LogInformation");        
        MyLogger.LogWarning("2: LogWarning");
        MyLogger.LogDebug("3: LogDebug");
        MyLogger.LogError("4: LogError");
        MyLogger.LogCritical("5: LogCritical");  //输出这个记录的日志, 浏览器底部不会出现一个Unhandled error 提示条
        MyLogger.LogInformation(new List<string> { ".A", ".B", ".C" });
        MyLogger.LogInformation(new {Name="张三",Age=20,Like="LOL"});
    }
}

=====================================

Serilog.Sinks.BrowserConsole 和 Serilog.Sinks.BrowserHttp

=====================================

参考 https://nblumhardt.com/2019/11/serilog-blazor/

这里仅仅介绍  Serilog.Sinks.BrowserConsole, 一般项目支持 appsettings.json 中配置logging属性, 但 blazor 项目不支持.

使用步骤:

1. 安装 nuget 包,

   dotnet add package Serilog.Sinks.BrowserConsole

2.  Program.cs 文件初始化全局 Serilog.Log.Logger 对象

3. 记日志方式, 不同于微软的 logger, 需要注入 logger 对象, serilog 直接使用 全局 Serilog.Log.Logger对象, 记录日志.

Program.cs 的代码:

using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Serilog.Configuration;
using Serilog;
using Serilog.Core;
using Serilog.Events;


namespace WebApplication2
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");


            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

            //设置 level 控制对象
             var logLevelSwitch = new LoggingLevelSwitch(LogEventLevel.Debug); //默认为 Info level

            //生成一个全局的 Logger
            Log.Logger = new LoggerConfiguration()
                .WriteTo.BrowserConsole()
                .MinimumLevel.ControlledBy(logLevelSwitch)
                .Enrich.WithProperty("InstanceId", Guid.NewGuid().ToString("n"))
                .CreateLogger();

            await builder.Build().RunAsync();
        }
    }
}

生成 Logger对象的写法, 有点特殊, 都是基于 LoggerConfiguration 类中多个角度的子配置完成的.

 

记录日志示例代码:

@page "/counter"
@using Serilog

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    class Person
    {
        public String Name { get; set; }
        public int Age { get; set; }
    }
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
        Log.Logger.Debug("Debug: Person data is {@Person}", new Person() { Name="Harry", Age=10});
        Log.Logger.Information("Info: Person2 data is {P2}", new   { Name = "Harry2", Age = 20 });
        Log.Logger.Information("Info:List is {@lst}", new List<String>() { "A", "B" });
        Log.Logger.Information("Info:List is {lst}", new List<String>() { "A", "B" }); } }

===============================

结论

===============================

推荐使用 Serilog.Sinks.BrowserConsole

<think>我们正在处理的是Blazor WebAssembly项目中的HTTP请求失败错误处理机制。用户希望避免在请求失败时直接报错,而是能够优雅地处理这些错误。 根据之前的讨论,我们已经实现了未授权处理(401),但HTTP请求可能因多种原因失败(如网络问题、服务器错误500等)。我们需要一个全局的错误处理机制来捕获这些异常并展示友好信息。 解决方案思路: 1. 使用自定义的DelegatingHandler拦截所有HTTP请求,在响应返回时检查状态码。 2. 根据不同的状态码(如401、403、500等)执行不同的处理逻辑(如重定向、显示错误信息等)。 3. 在组件中,我们也可以使用try-catch来捕获异常,但全局处理可以避免在每个调用处重复代码。 具体步骤: 一、创建全局HTTP错误处理拦截器 我们将扩展之前用于处理未授权的AuthTokenHandler,使其能够处理更多的HTTP错误状态。 修改后的AuthTokenHandler(重命名为ApiErrorHandler)如下: ```csharp // ApiErrorHandler.cs public class ApiErrorHandler : DelegatingHandler { private readonly NavigationManager _navigation; private readonly IAccessTokenProvider _tokenProvider; private readonly ILogger<ApiErrorHandler> _logger; public ApiErrorHandler( NavigationManager navigation, IAccessTokenProvider tokenProvider, ILogger<ApiErrorHandler> logger) { _navigation = navigation; _tokenProvider = tokenProvider; _logger = logger; } protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { HttpResponseMessage response = null; try { // 尝试获取访问令牌(仅当请求需要认证时,可以根据请求的URL判断是否需要,但这里我们简单处理,所有请求都尝试添加) var tokenResult = await _tokenProvider.RequestAccessToken(); if (tokenResult.TryGetToken(out var token)) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Value); } // 注意:即使没有token,我们也发送请求,因为有些API可能不需要认证,但服务器会返回401,我们会在下面处理 response = await base.SendAsync(request, cancellationToken); // 根据响应状态码处理 if (!response.IsSuccessStatusCode) { await HandleErrorResponse(response); } } catch (Exception ex) // 捕获请求发送过程中发生的异常(如网络不可用) { await HandleExceptionAsync(ex); } return response; // 注意:如果请求过程中出现异常,response可能为null,但这种情况很少 } private async Task HandleErrorResponse(HttpResponseMessage response) { if (response.StatusCode == HttpStatusCode.Unauthorized) // 401 { // 跳转到登录页面 var returnUrl = _navigation.Uri; _navigation.NavigateTo($"/login?returnUrl={Uri.EscapeDataString(returnUrl)}"); } else if (response.StatusCode == HttpStatusCode.Forbidden) // 403 { // 跳转到无权限页面 _navigation.NavigateTo("/forbidden"); } else { // 其他错误(如500),我们可以显示一个错误页面,或者记录日志 _logger.LogError($"HTTP Error: {response.StatusCode}, URL: {response.RequestMessage.RequestUri}"); // 这里我们可以抛出一个自定义异常,以便在组件中捕获,或者使用全局状态管理错误信息 throw new HttpRequestException($"HTTP Error: {response.StatusCode}"); } } private async Task HandleExceptionAsync(Exception exception) { _logger.LogError(exception, "HTTP请求发生异常"); // 这里可以导航到错误页面,或者记录后重新抛出 // 如果是网络错误,我们可以提示用户检查网络 // 由于在SendAsync方法中已经捕获,我们可以在这里设置一个全局错误状态,然后在UI上显示 // 或者使用一个全局的弹窗服务来显示错误 // 例如:使用一个Toast服务显示“网络连接失败,请重试” // 我们这里先简单处理,抛出一个自定义异常 throw new ApplicationException("网络请求失败,请检查网络连接。", exception); } } ``` 二、服务注册(Program.cs) 注册这个自定义的Handler,并应用到HttpClient上。 ```csharp // Program.cs builder.Services.AddScoped<ApiErrorHandler>(); builder.Services.AddHttpClient("SecureAPI", client => { client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress); // 或者你的API基础地址 }) .AddHttpMessageHandler<ApiErrorHandler>(); // 添加错误处理器 ``` 三、在组件中使用HttpClient 在组件中,我们使用这个命名的HttpClient("SecureAPI")来发送请求。这样,所有的请求都会被我们的错误处理器拦截。 但是,即使有全局的错误处理器,我们可能仍然需要在组件中处理一些特定的错误(例如,在调用某个API失败时,需要重置组件的加载状态)。因此,我们可以在组件中使用try-catch。 ```csharp @inject IHttpClientFactory HttpClientFactory <button @onclick="LoadData">加载数据</button> @if (isLoading) { <p>加载中...</p> } else if (errorMessage != null) { <p class="text-danger">@errorMessage</p> } else { // 显示数据 } @code { private bool isLoading = false; private string errorMessage = null; private async Task LoadData() { isLoading = true; errorMessage = null; try { var httpClient = HttpClientFactory.CreateClient("SecureAPI"); // 发送请求... // 这里我们假设请求失败已经被ApiErrorHandler处理了,但是可能有些错误我们想在组件内处理 // 例如,如果ApiErrorHandler对于500错误抛出了异常,我们在这里捕获 var response = await httpClient.GetAsync("/api/data"); response.EnsureSuccessStatusCode(); // 如果状态码不是2xx,会抛出异常 // ... 处理响应 } catch (HttpRequestException ex) { // 这里可以捕获到非2xx的状态码(但注意,我们的ApiErrorHandler已经处理了401和403,并进行了跳转,所以这里不会捕获到401和403) // 因为ApiErrorHandler在遇到401/403时已经导航走了,所以组件内的代码不会继续执行(除非导航被取消,但通常不会) // 所以这里捕获的可能是其他错误,比如500 errorMessage = "服务器错误,请稍后重试"; } catch (ApplicationException ex) // 我们自定义的网络异常 { errorMessage = ex.Message; } finally { isLoading = false; } } } ``` 四、全局错误处理(可选) 如果我们希望有一个全局的错误处理机制(例如,在发生未处理的异常时显示一个错误页面),我们可以使用Blazor的ErrorBoundary组件(.NET 6及以上)或自定义错误处理。 在App.razor中,我们可以使用ErrorBoundary包裹整个应用: ```razor <Router AppAssembly="@typeof(Program).Assembly"> <Found Context="routeData"> <ErrorBoundary> <ChildContent> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </ChildContent> <ErrorContent> <p class="error">抱歉,发生了一个意外错误。</p> </ErrorContent> </ErrorBoundary> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>抱歉,未找到您请求的页面。</p> </LayoutView> </NotFound> </Router> ``` 但是注意,ErrorBoundary只能捕获渲染过程中的异常,不能捕获事件中的异常(比如按钮点击事件中的异常)。对于事件中的异常,我们需要自己捕获并处理,或者使用全局的异常处理: 在Program.cs中注册全局异常处理: ```csharp // 在Program.cs中 builder.Services.AddScoped<ErrorBoundaryLogger>(); builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging")); // 全局事件异常处理(仅捕获UI事件中的异常) builder.Services.AddCascadingValue(sp => new ErrorRecoveryService(sp.GetRequiredService<ILogger<ErrorRecoveryService>>())); // 自定义服务,用于在事件中捕获异常并记录 public class ErrorRecoveryService { private readonly ILogger<ErrorRecoveryService> _logger; public ErrorRecoveryService(ILogger<ErrorRecoveryService> logger) { _logger = logger; } public void HandleException(Exception ex) { _logger.LogError(ex, "发生未处理的异常"); // 这里可以弹出一个全局错误提示 } } ``` 然后在组件的事件中,我们可以这样使用: ```csharp @inject ErrorRecoveryService ErrorRecovery private async Task LoadData() { try { // ... 代码 } catch (Exception ex) { ErrorRecovery.HandleException(ex); } } ``` 但是这样每个事件都要写try-catch,比较麻烦。另一种方式是实现一个全局的事件处理包装器,但这比较复杂。 总结: 1. 使用自定义的DelegatingHandler(ApiErrorHandler)拦截HTTP请求,处理错误状态码和网络异常。 2. 在组件中使用try-catch来捕获可能出现的异常,并更新组件的状态(显示错误信息)。 3. 使用ErrorBoundary处理渲染过程中的异常。 4. 对于事件中的异常,可以创建一个全局的错误处理服务,在事件中手动调用。 这样,我们就可以在Blazor WebAssembly中实现一个健壮的错误处理机制。 引用说明: 在Blazor WebAssembly中,我们使用HttpClient进行网络请求,而通过DelegatingHandler我们可以拦截请求和响应,这是处理HTTP错误的理想位置[^1][^2]。 另外,fullstackhero Blazor WebAssembly Boilerplate项目中也采用了类似的处理机制[^3]。 同时,我们注意到在Blazor WebAssembly项目中,由于运行在浏览器中,网络请求可能受到同源策略的限制,但我们的项目是托管在ASP.NET Core服务器中,所以不会违反同源策略[^4]。 相关问题: 1. 如何在Blazor WebAssembly中实现全局的异常处理(包括UI事件)? 2. 如何为Blazor WebAssembly项目配置不同的HttpClient? 3. 在Blazor WebAssembly中,如何优雅地处理网络连接失败的情况? 4. 如何记录Blazor WebAssembly中的错误日志并发送到服务器? 5. 在Blazor WebAssembly中,ErrorBoundary组件能捕获哪些类型的异常?</think>在 Blazor WebAssembly 项目中优雅地处理 HTTP 请求失败情况,可以通过以下全局错误处理机制实现,避免直接报错: ### 一、全局 HTTP 拦截器实现错误处理 ```csharp // ApiErrorHandler.cs public class ApiErrorHandler : DelegatingHandler { private readonly NavigationManager _navigation; private readonly ILogger<ApiErrorHandler> _logger; private readonly ToastService _toastService; // 通知服务 public ApiErrorHandler( NavigationManager navigation, ILogger<ApiErrorHandler> logger, ToastService toastService) { _navigation = navigation; _logger = logger; _toastService = toastService; } protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { HttpResponseMessage response = null; try { response = await base.SendAsync(request, cancellationToken); // 处理非成功状态码 if (!response.IsSuccessStatusCode) { await HandleErrorResponse(response); } } catch (HttpRequestException ex) // 网络请求异常 { _logger.LogError(ex, "网络请求失败"); _toastService.ShowError("网络连接失败,请检查网络"); } catch (TaskCanceledException) // 请求超时 { _toastService.ShowWarning("请求超时,请重试"); } catch (Exception ex) // 其他异常 { _logger.LogCritical(ex, "未处理的HTTP异常"); _toastService.ShowError("系统错误,请联系管理员"); } return response; } private async Task HandleErrorResponse(HttpResponseMessage response) { switch (response.StatusCode) { case HttpStatusCode.Unauthorized: // 401 _navigation.NavigateTo($"/login?returnUrl={Uri.EscapeDataString(_navigation.Uri)}"); break; case HttpStatusCode.Forbidden: // 403 _toastService.ShowError("无访问权限"); break; case HttpStatusCode.NotFound: // 404 _toastService.ShowWarning("请求的资源不存在"); break; case HttpStatusCode.BadRequest: // 400 var errors = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>(); ShowValidationErrors(errors); break; default: // 500等服务器错误 _toastService.ShowError($"服务器错误: {(int)response.StatusCode}"); break; } } private void ShowValidationErrors(ValidationProblemDetails details) { foreach (var error in details.Errors) { _toastService.ShowWarning($"{error.Key}: {string.Join(", ", error.Value)}"); } } } ``` ### 二、服务注册配置(Program.cs) ```csharp // 注册错误处理器 builder.Services.AddScoped<ApiErrorHandler>(); // 配置HttpClient builder.Services.AddHttpClient("ApiClient", client => { client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress); client.Timeout = TimeSpan.FromSeconds(30); // 设置超时时间 }) .AddHttpMessageHandler<ApiErrorHandler>(); // 添加错误处理器 // 注册通知服务(使用MudBlazor示例) builder.Services.AddScoped<ToastService>(); ``` ### 三、组件级错误处理 ```razor @inject IHttpClientFactory HttpClientFactory <MudButton @onclick="LoadData" Disabled="@isLoading"> 加载数据 </MudButton> @if (isLoading) { <MudProgressLinear Indeterminate="true" /> } @if (errorMessage != null) { <MudAlert Severity="Severity.Error">@errorMessage</MudAlert> } @code { private bool isLoading; private string errorMessage; private async Task LoadData() { isLoading = true; errorMessage = null; try { var httpClient = HttpClientFactory.CreateClient("ApiClient"); var response = await httpClient.GetFromJsonAsync<DataModel>("/api/data"); // 处理数据... } catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { // 特殊处理404错误 errorMessage = "请求的数据不存在"; } catch (Exception ex) { errorMessage = $"加载失败: {ex.Message}"; } finally { isLoading = false; } } } ``` ### 四、全局异常边界处理 ```razor // App.razor <Router AppAssembly="@typeof(Program).Assembly"> <Found Context="routeData"> <ErrorBoundary> <ChildContent> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </ChildContent> <ErrorContent> <MudContainer> <MudAlert Severity="Severity.Error"> 发生未处理的异常 </MudAlert> <MudButton @onclick="Recover">返回首页</MudButton> </MudContainer> </ErrorContent> </ErrorBoundary> </Found> </Router> @code { [CascadingParameter] private ErrorBoundary? ErrorBoundary { get; set; } private void Recover() { ErrorBoundary?.Recover(); NavigationManager.NavigateTo("/"); } } ``` ### 关键处理策略: 1. **分层错误处理** - 网络层:通过 `DelegatingHandler` 拦截所有 HTTP 请求[^1] - 组件层:使用 try-catch 处理特定业务逻辑错误 - 全局层:使用 `ErrorBoundary` 捕获渲染异常 2. **错误分类处理** ```mermaid graph TD A[HTTP错误] --> B[4xx客户端错误] A --> C[5xx服务器错误] B --> D[401 重定向登录] B --> E[403 权限提示] B --> F[404 资源提示] C --> G[500+ 服务错误提示] ``` 3. **用户体验优化** - 使用 Toast 通知显示非关键错误 - 为长时间操作添加加载指示器 - 提供明确的错误恢复操作 4. **日志记录** - 关键错误记录到浏览器控制台 - 生产环境可集成 Application Insights > **最佳实践**:在 `Program.cs` 中配置重试策略增强鲁棒性: > ```csharp > builder.Services.AddHttpClient("RetryClient") > .AddTransientHttpErrorPolicy(policy => > policy.WaitAndRetryAsync(3, retry => TimeSpan.FromSeconds(Math.Pow(2, retry))) > .AddHttpMessageHandler<ApiErrorHandler>(); > ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值