如何在ASP.NET 6 Blazor Server项目中读取自身及依赖库日志
实现Blazor Server项目内展示自身及依赖类库的日志(轮询方式)
问题说明
我有一个ASP.NET 6 Blazor Server项目,在WeatherForecastService中注入了日志组件;同时引用了一个类库项目,在ASP.NET项目里实例化类库的Dog对象并传递日志实例。调试时两个项目的日志都会输出到VS的输出面板,现在想在不关闭该输出的前提下,在Blazor项目的“日志”页面展示所有日志(包括自身和类库的)。打算用每秒轮询服务器获取新日志的方式,需要能捕获所有产生的日志,就像VS输出面板那样,请问怎么实现?
项目代码片段:
Blazor Server项目代码
namespace BlazorApp1.Data { public class WeatherForecastService { private readonly ILogger _logger; public WeatherForecastService(ILogger<WeatherForecastService> logger, IServiceProvider sp) { _logger = logger; var d = new Dog(sp.GetRequiredService<ILogger<Dog>>()); logger.LogInformation("WeatherForecastService created."); } } }
引用的类库项目代码
using Microsoft.Extensions.Logging; namespace ClassLibrary1 { public class Dog { public Dog(ILogger<Dog> logger) { logger.LogInformation("Dog created."); } } }
实现方案
通过自定义日志收集器包装原日志提供器,既保留VS输出面板的日志输出,又将所有日志收集到内存中,再通过Blazor页面轮询获取并展示。
1. 定义日志模型与收集服务
日志条目模型
public class LogEntry { public DateTime Timestamp { get; set; } public LogLevel Level { get; set; } public string Category { get; set; } public string Message { get; set; } }
日志收集核心服务(单例)
用于存储日志并提供轮询所需的新日志获取方法,保证线程安全:
public class LogCollector { private readonly ConcurrentQueue<LogEntry> _allLogs = new(); private int _lastRetrievedIndex = 0; public void AddLog(LogEntry entry) { _allLogs.Enqueue(entry); } // 获取上次查询后新增的日志 public List<LogEntry> GetNewLogs() { var logs = _allLogs.Skip(_lastRetrievedIndex).ToList(); _lastRetrievedIndex += logs.Count; return logs; } // 获取所有日志(页面初始化时用) public List<LogEntry> GetAllLogs() { _lastRetrievedIndex = _allLogs.Count; return _allLogs.ToList(); } }
2. 自定义日志提供器与Logger
包装原有的DebugLoggerProvider(VS输出面板使用的提供器),在保留原输出的同时收集日志:
自定义Logger
public class CollectingLogger : ILogger { private readonly string _categoryName; private readonly LogCollector _logCollector; private readonly ILogger _originalLogger; public CollectingLogger(string categoryName, LogCollector logCollector, ILogger originalLogger) { _categoryName = categoryName; _logCollector = logCollector; _originalLogger = originalLogger; } public IDisposable BeginScope<TState>(TState state) => _originalLogger.BeginScope(state); public bool IsEnabled(LogLevel logLevel) => _originalLogger.IsEnabled(logLevel); public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { // 先让原日志提供器输出到VS面板 _originalLogger.Log(logLevel, eventId, state, exception, formatter); // 收集日志到内存 if (IsEnabled(logLevel)) { var message = formatter(state, exception); _logCollector.AddLog(new LogEntry { Timestamp = DateTime.Now, Level = logLevel, Category = _categoryName, Message = message }); } } }
自定义LoggerProvider
public class CollectingLoggerProvider : ILoggerProvider { private readonly LogCollector _logCollector; private readonly ILoggerProvider _originalProvider; private readonly Dictionary<string, CollectingLogger> _loggers = new(); public CollectingLoggerProvider(LogCollector logCollector, ILoggerProvider originalProvider) { _logCollector = logCollector; _originalProvider = originalProvider; } public ILogger CreateLogger(string categoryName) { lock (_loggers) { if (!_loggers.TryGetValue(categoryName, out var logger)) { var originalLogger = _originalProvider.CreateLogger(categoryName); logger = new CollectingLogger(categoryName, _logCollector, originalLogger); _loggers[categoryName] = logger; } return logger; } } public void Dispose() { _originalProvider.Dispose(); lock (_loggers) { _loggers.Clear(); } } }
3. 注册服务到DI容器
在Program.cs中替换原日志提供器,注册自定义收集服务:
var builder = WebApplication.CreateBuilder(args); // 注册日志收集器为单例 builder.Services.AddSingleton<LogCollector>(); // 替换Debug日志提供器为自定义包装器,保留VS输出 var originalDebugProvider = builder.Logging.Services .FirstOrDefault(s => s.ServiceType == typeof(ILoggerProvider) && s.ImplementationType == typeof(DebugLoggerProvider)); if (originalDebugProvider != null) { builder.Logging.Services.Remove(originalDebugProvider); builder.Logging.AddProvider(serviceProvider => { var logCollector = serviceProvider.GetRequiredService<LogCollector>(); var originalProvider = new DebugLoggerProvider(); return new CollectingLoggerProvider(logCollector, originalProvider); }); } // 其他原有服务注册 builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); builder.Services.AddSingleton<WeatherForecastService>(); var app = builder.Build(); // ... 中间件配置省略 app.Run();
4. 创建Blazor日志展示页面
新建Logs.razor页面,实现每秒轮询并展示日志:
@page "/logs" @inject LogCollector LogCollector @inject IJSRuntime JSRuntime <h3>应用日志</h3> <div id="log-container" style="height: 500px; overflow-y: auto; border: 1px solid #ccc; padding: 10px;"> @foreach (var log in AllLogs) { <div class="log-item @GetLogLevelClass(log.Level)"> <span>@log.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff")</span> <span class="mx-2">[@log.Level]</span> <span class="text-muted">(@log.Category)</span> <span class="mx-2">|</span> <span>@log.Message</span> </div> } </div> @code { private List<LogEntry> AllLogs = new(); protected override async Task OnInitializedAsync() { // 初始化加载所有已存在的日志 AllLogs = LogCollector.GetAllLogs(); // 启动轮询任务 _ = PollForNewLogs(); } private async Task PollForNewLogs() { while (true) { await Task.Delay(1000); // 每秒轮询一次 var newLogs = LogCollector.GetNewLogs(); if (newLogs.Any()) { AllLogs.AddRange(newLogs); StateHasChanged(); // 自动滚动到日志底部 await JSRuntime.InvokeVoidAsync("scrollToBottom", "log-container"); } } } private string GetLogLevelClass(LogLevel level) { return level switch { LogLevel.Trace => "text-secondary", LogLevel.Debug => "text-info", LogLevel.Information => "text-success", LogLevel.Warning => "text-warning", LogLevel.Error => "text-danger", LogLevel.Critical => "text-danger fw-bold", _ => "" }; } }
在_Host.cshtml中添加滚动用的JS函数:
<script> function scrollToBottom(elementId) { const element = document.getElementById(elementId); if (element) { element.scrollTop = element.scrollHeight; } } </script>
注意事项
- 线程安全:使用
ConcurrentQueue保证多线程环境下的日志收集安全 - 日志清理:长期运行的应用建议添加日志清理逻辑(比如保留最近1000条或24小时内的日志)
- 轮询频率:可根据实际需求调整轮询间隔,1秒为适中频率
- 日志过滤:可在
CollectingLogger的Log方法中添加过滤逻辑,只收集指定级别或类别的日志
内容的提问来源于stack exchange,提问作者Damn Vegetables
相关产品推荐
相关产品推荐

