You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 06:15:40