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

接下来就需要在中配置.

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; }

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

public class SysExceptionFilter : IAsyncExceptionFilter
{
readonly IHubContext _hub;
//使用log4
ILog _log = LogManager.GetLogger(Startup.Logger.Name, typeof(SysExceptionFilter));
public SysExceptionFilter(IHubContext 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 });
}
}

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

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

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

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

log4net.dll_log4net_log4NET写入原理

2、读取日志文件

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

log4NET写入原理_log4net.dll_log4net

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

<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
{
///
/// https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.readerwriterlockslim?view=netframework-4.8
/// 这里主要控制控制多个线程读取日志文件
///

static ReaderWriterLockSlim _slimLock = new ReaderWriterLockSlim();
public static List Read(string filePath="")
{
//日志对象集合
List datas = new List();
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("rn", "").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
{
///
/// 时间
///

public DateTime CreateTime { get; set; }
///
/// 日志级别
///

public string Level { get; set; }
///
/// 日志描述
///

public string Summary { get; set; }
}

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

来一段原文描述:

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

虽然可以简单的使用一个排它锁来保护对实例的任何形式的访问。

但是如果其读操作很多但是更新操作很少,则使用单一的锁限制并发性就不大合理了。

这种情况出现在业务应用服务器上,它会将常用的数据缓存在静态字段中进行快速检索。

是专门为这种情形设计的log4net,它可以最大限度的保证锁的可用性。

在.net3.5引入的它替代了笨重的类。

虽然两者功能相识,但是后者的执行速度比前置慢数倍。

和都拥有两种基本锁,读和写。

写锁是全局排它锁

读锁可以兼容其他的锁

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

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

这是关于官网最新的描述:

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

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

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

log4net.dll_log4net_log4NET写入原理

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

成功进入断点

log4net_log4NET写入原理_log4net.dll

shift+f9监听data看看数据

log4net.dll_log4NET写入原理_log4net

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

3、实时发送日志数据

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

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

强类型中心:

#-the-name-of-a-hub-

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

/// 
/// https://docs.microsoft.com/zh-cn/aspnet/core/signalr/hubs?view=aspnetcore-3.1
/// 强类型中心
///

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
{
///
/// 给所有客户端发送消息
///

/// 用户
/// 消息
///
public async Task SendMessage(string user, string message)
{
await Clients.All.ReceiveMessage(user, message);
}
///
/// 向调用客户端发送消息
///

///
///
public async Task SendMessageCaller(string message)
{
await Clients.Caller.ReceiveCaller( message);
}
///
/// 客户端连接服务端
///

///
public override Task OnConnectedAsync()
{
var id = Context.ConnectionId;
//_logger.Info($"客户端ConnectionId=>【{id}】已连接服务器!");
return base.OnConnectedAsync();
}
///
/// 客户端断开连接
///

///
///
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:这个改动不会影响它在控制器注入,或者其它注入地方的使用。

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

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

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

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

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

官网:#/zh-CN//

npm i element-ui -S

在mian.js添加配置

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>

启动看看效果。

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

log4net_log4NET写入原理_log4net.dll

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

log4net_log4net.dll_log4NET写入原理

来个gif看看效果

log4NET写入原理_log4net_log4net.dll

结语

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

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

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

Dome源码地址:

学习使我快乐!!!

- EOF -

技术群:添加小编微信

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注