系列

源码地址:https://github.com/QQ2287991080/SignalRServerAndVueClientDemo

效果

老规矩先看最后效果

vue +signalR+log4net  实时日志推送-LMLPHP

步骤

1、配置log4net日志

实现日志推送,首先需要配置log4net日志,然后定义一个全局异常捕获器,用于捕获错误写入到日志文件。

vue +signalR+log4net  实时日志推送-LMLPHP

 先把nuget包安装一下。

然后需要配置log4net的xml信息,右键web项目“添加”->“新建项”

vue +signalR+log4net  实时日志推送-LMLPHP

找到Web配置文件->“命名”->"点击添加"

 vue +signalR+log4net  实时日志推送-LMLPHP

 然后把xml配置放入到config文件中,配置如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <log4net>
    <appender name="DebugAppender" type="log4net.Appender.DebugAppender" >
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
      </layout>
    </appender>
    <!--全局异常日志-->
    <appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
      <!--日志文件存放位置-->
      <file value="../../../logs/system.log" />
      <!--是否追加到日志文件中-->
      <appendToFile value="true" />
      <!--基于文件大小滚动设置-->
      <rollingStyle value="Composite" />
      <!--是否指定了日志文件名称-->
      <staticLogFileName value="true" />
      <!--根据日期生成日志文件-->
      <!--<datePattern value="yyyyMMdd'.log'" />-->
      <!--最多保留10个旧文件-->
      <maxSizeRollBackups value="10" />
      <!--日志文件的大小-->
      <maximumFileSize value="1GB" />
      <layout type="log4net.Layout.PatternLayout">
        <!--日志模板,这个东西很重要后续读取日志文件的时候就是依据这个配置-->
        <conversionPattern value="%n时间:%date{yyyy-MM-dd HH:mm:ss},%n线程Id:%thread,%n日志级别:%-5level,%n描述:%message|%newline"/>
      </layout>
    </appender>
    <root>
      <level value="All"/>
      <appender-ref ref="DebugAppender" />
      <appender-ref ref="RollingFile"  />
    </root>
  </log4net>
</configuration>

想要更多配置的可以前往官网:http://logging.apache.org/log4net/release/config-examples.html  

 如果对生成多个文件夹有兴趣的可以看我另外:Asp.Net Core Log4Net 配置分多个文件记录日志(不同日志级别)

接下来就需要在Startup中配置log4net.

public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
            Logger = LogManager.CreateRepository(Assembly.GetEntryAssembly(), typeof(log4net.Repository.Hierarchy.Hierarchy));
            XmlConfigurator.Configure(Logger, new FileInfo("log4net.config"));
           // _logger = LogManager.GetLogger(Logger.Name, typeof(Startup));
        }

        public static ILoggerRepository Logger { get; set; }

按照我最开始说的,在配置好日志之后需要配置一个全局错误捕获器,直接上代码。

 public class SysExceptionFilter : IAsyncExceptionFilter
    {
        readonly IHubContext<ChatHub> _hub;
        //使用log4
        ILog _log = LogManager.GetLogger(Startup.Logger.Name, typeof(SysExceptionFilter));
        public SysExceptionFilter(IHubContext<ChatHub> hub)
        {
            _hub = hub;
        }
        public async Task OnExceptionAsync(ExceptionContext context)
        {
            //错误
            var ex = context.Exception;
            //错误信息
            string message = ex.Message;
            //请求方法的路由
            string url = context.HttpContext?.Request.Path;
            //写入日志文件描述  注意这个地方尽量不要用中文冒号,否则读取日志文件的时候会造成信息确实,当然你可以定义自己的规则
            string logMessage = $"错误信息=>【{message}】,【请求地址=>{url}】";
            //写入日志
            _log.Error(logMessage);
            //读取日志
            var data = ReadHelper.Read();
            //发送给客户端
            await _hub.Clients.All.SendAsync("ReceiveLog", data);
            //返回一个正确的200http码,避免前端错误
            context.Result = new JsonResult(new { ErrCode = 0, ErrMsg = message, Data = true });
        }
    }

代码中的读取日志会在第二节中讲到。

在Startup服务中注册这个过滤器。

 public void ConfigureServices(IServiceCollection services)
        {
            ......
            services.AddMvc(option =>
            {
                //添加错误捕获
                option.Filters.Add(typeof(SysExceptionFilter));
                //option.EnableEndpointRouting = false;
            });
           ......
        }

按照我这个配置将会在程序目录生成一个logs文件夹,以及一个system.log文件。

vue +signalR+log4net  实时日志推送-LMLPHP

2、读取日志文件

在配置日志文件中已经将日志配置了,再看看生成日志文件内容。

vue +signalR+log4net  实时日志推送-LMLPHP

 跟我在log4net.config中配置的是一样的。

 <layout type="log4net.Layout.PatternLayout">
        <!--日志模板,这个东西很重要后续读取日志文件的时候就是依据这个配置-->
        <conversionPattern value="%n时间:%date{yyyy-MM-dd HH:mm:ss},%n线程Id:%thread,%n日志级别:%-5level,%n描述:%message|%newline"/>
      </layout>

然后需要读取日志文件的,把日志文件的内容转换成前端能够识别的数据。

public class ReadHelper
    {
        /// <summary>
        /// https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.readerwriterlockslim?view=netframework-4.8
        /// 这里主要控制控制多个线程读取日志文件
        /// </summary>
        static ReaderWriterLockSlim _slimLock = new ReaderWriterLockSlim();

        public static List<SysExceptionData> Read(string filePath="")
        {
            //日志对象集合
            List<SysExceptionData> datas = new List<SysExceptionData>();
            filePath = Directory.GetCurrentDirectory() + "\\logs\\system.log";
            //判断日志文件是否存在
            if (!File.Exists(filePath))
            {
                return datas;
            }
            _slimLock.EnterReadLock();
            try
            {

                //获取日志文件流
                var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
                //读取内容
                var reader = new StreamReader(fs);
                var content = reader.ReadToEnd();
                reader.Close();
                fs.Close();
                /*
                 *处理内容,换行符替换掉,然后在log4net配置文件中在每一写入日志结尾的地方加上 |
                 *这样做的好处是便于在读取日志文件的时候处理日志数据返回给客户端
                 *由于是在每一行结束的地方加上| 所有根据Split分割之后最后一个数据必然是空的
                 *所有Where去除一下。
                 */
                var contentList = content.Replace("\r\n", "").Split('|').Where(w => !string.IsNullOrEmpty(w));
                foreach (var item in contentList)
                {
                    //根据逗号分割单个日志数据的内容
                    var info = item.Split(',');
                    //实例化日志对象
                    SysExceptionData data = new SysExceptionData();
                    data.CreateTime = Convert.ToDateTime(info[0].Split('')[1]);
                    data.Level = info[2].Split('')[1];
                    data.Summary = info[3].Split('')[1];
                    datas.Add(data);
                }
            }
            finally
            {
                //退出
                _slimLock.ExitReadLock();
            }
            return datas.OrderByDescending(bo=>bo.CreateTime).ToList();
        }
    }
    public class SysExceptionData
    {
        /// <summary>
        /// 时间
        /// </summary>
        public DateTime CreateTime { get; set; }
        /// <summary>
        /// 日志级别
        /// </summary>
        public string Level { get; set; }
        /// <summary>
        /// 日志描述
        /// </summary>
        public string Summary { get; set; }
    }

这里需要说一下的是为什么要用ReaderWriterLockSlim,其实在写这篇博客之前我刚好看书学到这个东西。

来一段原文描述:


通常一个类型实例的并发读操作是线程安全的,而并发更新操作则不是。诸如文件这样的资源也具有相同的特点。

虽然可以简单的使用一个排它锁来保护对实例的任何形式的访问。
但是如果其读操作很多但是更新操作很少,则使用单一的锁限制并发性就不大合理了。
这种情况出现在业务应用服务器上,它会将常用的数据缓存在静态字段中进行快速检索。
ReaderWriterLockSlim是专门为这种情形设计的,它可以最大限度的保证锁的可用性。ReaderWriterLockSlim在.net3.5引入的它替代了笨重的ReaderWriterLock类。虽然两者功能相识,但是后者的执行速度比前置慢数倍。ReaderWriteLockSlim和ReaderWriterLock都拥有两种基本锁,读和写。

写锁是全局排它锁
读锁可以兼容其他的锁

因此,一个持有写锁的线程将阻塞其他任何试图获取读锁或写锁的京城。但是如果没有任何线程持有写锁的话,那么任意数量的线程都可以获得读锁。

ReaderWriterLockSlim和lock一样也有类似TryEnter之类的方法,来判断是否超时,如果超时就抛出错误(lock返回false)


这是关于ReaderWriterLockSlim官网最新的描述:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.readerwriterlockslim?view=netframework-4.8

对了,我看的是孔雀鸟--《c# 7.0核心技术指南》c#想进阶强烈推荐这本书。

同时这部分代码也有参考老张Blog.Core的源码,感谢!


接下来调试一下看看读取日志文件处理后的数据,我在TestController加了故意抛出错误的接口。

vue +signalR+log4net  实时日志推送-LMLPHP

 直接在浏览器输入 :http://localhost:13989/api/test/getLog

vue +signalR+log4net  实时日志推送-LMLPHP

 成功进入断点

vue +signalR+log4net  实时日志推送-LMLPHP

 shift+f9监听data看看数据

vue +signalR+log4net  实时日志推送-LMLPHP

 拿到这个数据,在客户端就直接可以用来展示,那么读取日志文件这部分就说完了,然后再说如何发送日志给客户端。

3、实时发送日志数据

 在日志过滤器中有这样一段代码,玩过signalr的人都知道SendAsync的第一个字符串其实是集线器中方法(Hub)的名称,但是我们也是可以自定义它的名称的。

//发送给客户端
 await _hub.Clients.All.SendAsync("ReceiveLog", data);

signalr强类型中心:https://docs.microsoft.com/zh-cn/aspnet/core/signalr/hubs?view=aspnetcore-3.1#change-the-name-of-a-hub-method

之前用的Hub不是强类型中心,这次一并给他改造了。

    /// <summary>
    /// https://docs.microsoft.com/zh-cn/aspnet/core/signalr/hubs?view=aspnetcore-3.1
    /// 强类型中心
    /// </summary>
    public interface IChatClient
    {
        Task ReceiveMessage(string user, string message);
        Task ReceiveMessage(object message);
        Task ReceiveCaller(object message);
        Task ReceiveLog(object data);
    }

重构源码之前的方法。

public class ChatHub : Hub<IChatClient>
    {
        /// <summary>
        /// 给所有客户端发送消息
        /// </summary>
        /// <param name="user">用户</param>
        /// <param name="message">消息</param>
        /// <returns></returns>
        public async Task SendMessage(string user, string message)
        {
            await Clients.All.ReceiveMessage(user, message);
        }
        /// <summary>
        /// 向调用客户端发送消息
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        public async Task SendMessageCaller(string message)
        {
            await Clients.Caller.ReceiveCaller( message);
        }

        /// <summary>
        /// 客户端连接服务端
        /// </summary>
        /// <returns></returns>
        public override Task OnConnectedAsync()
        {
            var id = Context.ConnectionId;
            //_logger.Info($"客户端ConnectionId=>【{id}】已连接服务器!");
            return base.OnConnectedAsync();
        }
        /// <summary>
        /// 客户端断开连接
        /// </summary>
        /// <param name="exception"></param>
        /// <returns></returns>
        public override Task OnDisconnectedAsync(Exception exception)
        {
            var id = Context.ConnectionId;
            //_logger.Info($"客户端ConnectionId=>【{id}】已断开服务器连接!");
            return base.OnDisconnectedAsync(exception);
        }
        public async Task ReceiveLog(object data)
        {
            data = ReadHelper.Read();
            await Clients.All.ReceiveLog(data);
        }
    }

ps:这个改动不会影响它在控制器注入,或者其它注入地方的使用。

其实服务端的配置差不多好了,现在需要想的是在客户端,首次进入页面的时候是应该手动给他调用一次发送日志,否则进入页面是没有数据的。

然后我在TestController中加上一个接口手动触发

       [HttpGet]
        public  async Task<JsonResult> GetLogMessage()
        {
            var data = ReadHelper.Read();
            await _hubContext.Clients.All.SendAsync("ReceiveLog", data);
            return new JsonResult(0);
        }

🆗,接下来需要把注意力集中到客户端上了,

之前的两篇博客我是没有安装element-ui的,这一次我为了展示数据省事,就打算直接用element-table展示数据好了。

element官网:https://element.eleme.cn/#/zh-CN/component/installation

npm i element-ui -S

在mian.js添加配置

//element 
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

vue 这里我不敢乱讲,这个我也不是很会,所以直接放代码了,我把客户端直接的代码进行了一下改造,加了个菜单,然后之前的内容都放在不同的菜单。

<template>
  <div class="home">
    <h1>服务端错误日志返回</h1>
    <button @click="sendErr">执行一个错误</button>
    <div class="table">
      <el-table :data="tableData" border style="width: 100%">
        <el-table-column type="index" label="序号" width="100"></el-table-column>
        <el-table-column prop="createTime" label="日期" width="180"></el-table-column>
        <el-table-column prop="level" label="级别" width="100"></el-table-column>
        <el-table-column prop="summary" label="描述" width="300"></el-table-column>
      </el-table>
    </div>
  </div>
</template>

<script>
// @ is an alias to /src
import HelloWorld from "@/components/HelloWorld.vue";
import * as signalR from "@aspnet/signalr";
export default {
  name: "Home",
  components: {
    HelloWorld,
  },
  data() {
    return {
      message: "", //消息
      connection: "", //signalr连接
      messages: [], //返回消息
      tableData: [],
    };
  },
  methods: {
    //发出一个错误
    sendErr: function () {
      this.$http.get("http://localhost:13989/api/test/getLog").then((resp) => {
        //console.log(resp);
      });
    },
    //获取系统日志
    getLog: function () {
      this.$http
        .get("http://localhost:13989/api/test/GetLogMessage")
        .then((res) => {
          console.log(res);
        });
    },
    getdatalist: function () {
      this.$http
        .get("http://localhost:13989/api/test/GetLogMessage")
        .then((res) => {
          // console.log(res);
          //this.tableData = res.data;
        })
        .catch((err) => {
          console.log(err);
        });
    },
  },
  computed: {},
  mounted: function () {
    let thisVue = this;
    this.connection = new signalR.HubConnectionBuilder()
      .withUrl("http://localhost:13989/chathub", {
        skipNegotiation: true,
        transport: signalR.HttpTransportType.WebSockets,
      })
      .configureLogging(signalR.LogLevel.Information)
      .build();

    this.connection.start();
    //连接日志发送事件

    this.connection.on("ReceiveLog", function (message) {
      console.log("listening receivelog");
      thisVue.tableData = message;
    });

    //初始化表格数据
    thisVue.getdatalist();
  },
};
</script>
<style scoped>
.table {
  margin: 20px;
}
</style>

启动看看效果。

这是日志接口展示的客户端页面

vue +signalR+log4net  实时日志推送-LMLPHP

 之前博客的内容在聊天中。。

vue +signalR+log4net  实时日志推送-LMLPHP

 来个gif看看效果

vue +signalR+log4net  实时日志推送-LMLPHP

结语

今天的分享到这里就结束了,内心觉得写一篇博客真不容易,从这个想法的萌芽到写demo去实现大概花了一周,不断地去看资料,研究源码。

俗话说,人不逼自己一下,不知道有多少潜力。

最后希望博客能够帮助到需要的人,后续还想研究下signalr 配置jwt,redis,sqlserver等。

Dome源码地址:https://github.com/QQ2287991080/SignalRServerAndVueClientDemo

学习使我快乐!!!

vue +signalR+log4net  实时日志推送-LMLPHP

09-24 20:34