.NET6在浏览器显示系统日志

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

.NET6在浏览器显示系统日志
image

在浏览器显示日志是根据https://github.com/lavspent/Lavspent.BrowserLogger为基础进行改造的。

下载该项目并运行Test

.NET6在浏览器显示系统日志
image-20220927101418043

运行效果

.NET6在浏览器显示系统日志
image

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

.NET6在浏览器显示系统日志
image

我们将在netcore项目中使用serilog并使用Lavspent.BrowserLogger将日志信息显示在浏览器上。

新建net6 webapi项目,并添加Serilog.AspNetCore包引用

.NET6在浏览器显示系统日志
image

在program中添加代码使用serilog

builder.Host.UseSerilog((context, logger) => {     logger.WriteTo.Console();    logger.WriteTo.File("Logs/log.txt");
});

在WeatherForecastController中添加代码输出日志

.NET6在浏览器显示系统日志
image

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

.NET6在浏览器显示系统日志
image

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

.NET6在浏览器显示系统日志
image

按照Lavspent.BrowserLogger使用说明.NET6在浏览器显示系统日志

添加使用代码

.NET6在浏览器显示系统日志
image

BrowserLoggerOptions选项从配置文件appsetting.json读取

{  "BrowserLog": {    "LogLevel": {      "Default": "Warning"    },    "ConsolePath": "con",    "WebConsole": {      "LogStreamUrl": "wss://localhost:44364/ls",  //注意:改成自己项目的端口,如果项目使用https前缀为wss,http前缀为ws      "ShowClassName": false    }  },  "AllowedHosts": "*"}

集成完成后,通过swagger触发测试方法

.NET6在浏览器显示系统日志
image

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

.NET6在浏览器显示系统日志
image

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

.NET6在浏览器显示系统日志
image

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

.NET6在浏览器显示系统日志
image

为了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

.NET6在浏览器显示系统日志
image

代码如下:

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接收到了日志信息:

.NET6在浏览器显示系统日志
image

我们在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接收到了报错日志信息,并提示我们报错的代码位置是哪一行,这在系统运行的时候是很有帮助的,开发人员不用去数据库、或者服务器日志文件就能看到报错的信息。

.NET6在浏览器显示系统日志
image

但是报错信息还是不够显眼,报错信息如果能变成红色显示就能很快区分开来;而且页面会一直显示接收到的日志信息,当接收到报错信息最好能断开接收器,这样就能停留在报错信息的位置,并去排查错误了。基于此,对Default.html改造一下。

Default.html改造完成后启动项目接着触发TestError,这时候我们看到报错信息已经变成了红色一目了然。

.NET6在浏览器显示系统日志
image

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

.NET6在浏览器显示系统日志
image

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

.NET6在浏览器显示系统日志
image

在日志接收页面的过滤器中输入过滤关键字:456(模拟报错用户),点击GetWeatherForecast、TestError。

.NET6在浏览器显示系统日志日志显示页面只显示包含[token:456]的报错信息。

真实项目中如果要设定一些日志的额外信息,可通Enrichment来设置,详细信息可查看:https://github.com/serilog/serilog/wiki/Enrichment。

示例源代码:https://github.com/fisherLB/WebApiBrowserLog