注:本示例参考 johnwas 的代码来实现 在项目部署运行时若系统报错,通常只能通过查看系统日志文件的方式来排查代码报错;这是一个非常不便的事情,通常需要登录服务器并找到系统日志文件,才能打开日志查看具体的日志信息;就算将日志记录到数据库或者elasticserach,查看起来也非常不便;若系统报错,直接打开浏览器就能看到报错信息,并确认报错的代码位置,这将非常有用非常酷。我们将实现这样的功能,netcore项目在浏览器输出日志实际中的效果如下:.

在浏览器显示日志是根据https://github.com/lavspent/Lavspent.BrowserLogger为基础进行改造的。
下载该项目并运行Test

运行效果

日志显示界面地址为http://localhost:5000/con,刷新http://localhost:5000/api/values,日志界面接收日志信息并实时显示效果如图

我们将在netcore项目中使用serilog并使用Lavspent.BrowserLogger将日志信息显示在浏览器上。
新建net6 webapi项目,并添加Serilog.AspNetCore包引用

在program中添加代码使用serilog
builder.Host.UseSerilog((context, logger) => {
logger.WriteTo.Console();
logger.WriteTo.File("Logs/log.txt");
});
在WeatherForecastController中添加代码输出日志

控制台和日志输出了代码中的日志信息,serilog启用正常。

将下载的Lavspent.BrowserLogger类库添加到webapi项目所在的解决方案中

按照Lavspent.BrowserLogger使用说明
添加使用代码

BrowserLoggerOptions选项从配置文件appsetting.json读取
{
"BrowserLog": {
"LogLevel": {
"Default": "Warning"
},
"ConsolePath": "con",
"WebConsole": {
"LogStreamUrl": "wss://localhost:44364/ls", //注意:改成自己项目的端口,如果项目使用https前缀为wss,http前缀为ws
"ShowClassName": false
}
},
"AllowedHosts": "*"
}
集成完成后,通过swagger触发测试方法

发现Browser Logger没有输出日志信息

发现是Serilog的使用问题,Serilog提供各种接收器(Sink)来处理日志输出到不同位置。在program中这选中代码F12。

Serilog提供了ConsoleSink、FileSink来处理将日志输出到控制台和输出到文件。

为了Serilog的日志信息输出到Browser Logger,我们需要自定义一个日志接收器。关于Serilog的接收器,可查看:https://github.com/serilog/serilog/wiki/Provided-Sinks;
如何自定义Serilog接收器,可查看:https://github.com/serilog/serilog/wiki/Developing-a-sink;
自定义Serilog接收器:
在类库项目中添加接收器类BrowserSink.cs;添加扩展类BrowserLoggerConfigurationExtensions.cs

代码如下:
using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting;
using System;
using System.IO;
using System.Text;
using Lavspent.BrowserLogger;
using Lavspent.BrowserLogger.Models;
using System.Collections.Generic;
using System.Threading;
namespace Serilog.Sinks.Browser
{
public class BrowserSink : ILogEventSink
{
readonly ITextFormatter _textFormatter;
string _outputTemplate;
public BrowserSink(
ITextFormatter textFormatter,
string outputTemplate)
{
_textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter));
_outputTemplate = outputTemplate;
}
private void RenderFullExceptionInfo(TextWriter textWriter, Exception exception)
{
Stack<Exception> se = new Stack<Exception>();
while (exception != null)
{
se.Push(exception);
exception = exception.InnerException;
}
while (se.TryPop(out exception ))
{
textWriter.Write("\n*** Exception Source:[{0}] ***\n\n{1}\n\n{2}\n",
exception.Source,
exception.Message,
exception.StackTrace);
}
}
public void Emit(LogEvent logEvent)
{
if (BrowserLoggerService.Instance == null)
return;
using (TextWriter textWriter = new StringWriter())
{
_textFormatter.Format(logEvent, textWriter);
LogEventPropertyValue ev;
Exception exception = logEvent.Exception;
if (exception != null)
{
if (logEvent.Properties.TryGetValue("EventId", out ev))
{
RenderFullExceptionInfo(textWriter, exception);
}
}
BrowserLoggerService.Instance.Enqueue(new LogMessageEntry
{
LogLevel = (Microsoft.Extensions.Logging.LogLevel)logEvent.Level,
TimeStampUtc = DateTime.UtcNow,
// ThreadId= Thread.CurrentThread.ManagedThreadId,
Name = "",
Message = textWriter.ToString()
});
}
}
}
}
using Serilog.Configuration;
using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting;
using Serilog.Formatting.Display;
using Serilog.Sinks.Browser;
using System;
namespace Serilog
{
public static class BrowserLoggerConfigurationExtensions
{
static readonly object DefaultSyncRoot = new object();
public const string DefaultOutputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} [sid:{CorrelationId}] {NewLine}{Exception}";
// public const string DefaultOutputTemplate = "{Message:lj}{NewLine}{Exception}[sid:{sid}][db:{db}]";
public static LoggerConfiguration Browser(
this LoggerSinkConfiguration sinkConfiguration,
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
string outputTemplate = DefaultOutputTemplate,
IFormatProvider formatProvider = null,
LoggingLevelSwitch levelSwitch = null)
{
if (sinkConfiguration is null) throw new ArgumentNullException(nameof(sinkConfiguration));
var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider);
return sinkConfiguration.Sink(new BrowserSink(formatter, outputTemplate),
restrictedToMinimumLevel,
levelSwitch);
}
}
}
在program中启用新定义的BrowserSink接收器,在UseSerilog修改成如下:
builder.Host.UseSerilog((context, logger) => {
logger.WriteTo.Console();
logger.WriteTo.File("Logs/log.txt");
logger.WriteTo.Browser();
});
在swagger触发测试方法,这时候Browser Logger接收到了日志信息:

我们在WeatherForecastController添加方法测试异常信息
/// <summary>
/// 添加方法测试异常信息
/// </summary>
/// <returns></returns>
[HttpGet("/TestError")]
public IActionResult TestError()
{
string result = string.Empty;
try
{
//数组Summaries的只有十个元素,超过数组边界,报错
result = Summaries[20];
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
}
return Ok(result);
}
启动项目,在swagger触发TestError,Browser Logger接收到了报错日志信息,并提示我们报错的代码位置是哪一行,这在系统运行的时候是很有帮助的,开发人员不用去数据库、或者服务器日志文件就能看到报错的信息。

但是报错信息还是不够显眼,报错信息如果能变成红色显示就能很快区分开来;而且页面会一直显示接收到的日志信息,当接收到报错信息最好能断开接收器,这样就能停留在报错信息的位置,并去排查错误了。基于此,对Default.html改造一下。
Default.html改造完成后启动项目接着触发TestError,这时候我们看到报错信息已经变成了红色一目了然。

点击CONNECTED,连接信息就会变成DISCONNETED,尝试触发测试方法,Browser Logger不再接收新的信息。

模拟不同人员使用系统,我们只关注触发报错的用户日志信息。修改控制代码

在日志接收页面的过滤器中输入过滤关键字:456(模拟报错用户),点击GetWeatherForecast、TestError。
日志显示页面只显示包含[token:456]的报错信息。
真实项目中如果要设定一些日志的额外信息,可通Enrichment来设置,详细信息可查看:https://github.com/serilog/serilog/wiki/Enrichment。
示例源代码:https://github.com/fisherLB/WebApiBrowserLog