摘要
本期主要以规则引擎业务实现为背景,介绍长任务运行日志如何在页面端持续显示更新,用最小的耗费跟踪复杂逻辑的执行情况以便于问题定位。
效果
换不多说先上效果,紧接着跟进实现及处理逻辑
实现思路及组件
长任务当然优先是websocket比较好用,如果用轮询遍历的方式实现也非不可,无非入库查询或缓存,但之所以没用这种,主要考虑存在增加服务器附加代码处理,不利于剥离、增加额外的客户端与服务器taceId联动逻辑处理,且追踪交互效果不够好、
- 💪前端,介绍显示日志主要采用react技术栈,组件为 “console-feed”,同样的劫持console.log是基操,但我这里更看重的是在日志输出的同时能够输出对象“像console对象输出一样”,,另外一个不幸就是深度2级以上的对象无法正常输出,另外一个就是可以自己实现一个console函数同样能够达成目的。
npm i console-feed
主要页面逻辑如下,分别为日志实现、WebSocket连接、日志输出格式预处理
const Log={};
Log.logs = [];
['log', 'dir','table','group','warn', 'info', 'error', 'debug', 'assert', 'time', 'timeEnd'].forEach(
method => {
Log[`$${method}`] = Log[method]
Log[method] = (mes) => {
// Log.logs.push(mes)
}
}
)
function createWebSocket(options:object){
options = options || {};
var socket;
socket = new WebSocket(options.url ||(location.origin.replace("http",'ws') + '/ws'));
socket.onopen = options.onopen.bind(this);
socket.onmessage = options.onmessage.bind(this);
socket.onerror = options.onerror || function(){
// layer.layer.msg('WebSocket错误');
console.warn('WebSocket错误')
}
return socket;
}
function debounce(fn, delay) {
let time = null;//time用来控制事件的触发
return function () {
if (time !== null) {
clearTimeout(time);
}
time = setTimeout(() => {
fn.call(this);
//利用call(),让this的指针从指向window 转成指向input
}, delay)
}
}
const ip=location.host;
socket =createWebSocket({
url:`ws://${ip}/ws`,
onopen : function(){
const flow=this.toJsonFlow();
socket.send(JSON.stringify({
eventType : 'debug',
message : JSON.stringify(flow)//editor.getXML()
}));
},
onmessage : function(e){
var event = JSON.parse(e.data);
var eventType = event.eventType;
var message = event.message;
const pushMessage=(event,type)=>{
var item=event.message;
switch(type){
case "log":
Log[item.level](`${event.timestamp}:\n${item.message}`);
break;
case "output":
Log.table( JSON.stringify(item));
break;
default:
Log.info(type);
break
}
if(eventType=="finish"){
this.running$.next(false);
}
}
const eventHandle={
'finish':pushMessage,
'output':pushMessage,
'log':pushMessage,
'debug':pushMessage,
}
const handle= eventHandle[eventType];
// if(handle)handle(event,eventType);
debounce(function () {
if(handle)handle(event,eventType);
}, 200)();
react监听处理逻辑
import { Console, Hook, Unhook } from 'console-feed'
useEffect(() => {
Hook(Log, log =>{
setLogs(currLogs =>[...currLogs, log])
document.querySelector(".consoleLog").scrollIntoView({behavior: "smooth", block: "end"});
}, false,1000)
})
return () => Unhook(Log)
}, [])
react渲染
<Console logs={logs} variant="dark" />
- 💪服务端主要思路是借用日志输出组件,通过重写日志append当先线程上下文做筛选判断,选择性的格式化日志输出内容入websocket中,达成即不破坏原逻辑,又可遴选记录日志内容的目的。
日志记录,原本该怎么记录日志就怎么记录
private static Logger logger = LoggerFactory.getLogger(Flower.class);
日志拦截
public class LogWebSocketAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
@Override
protected void append(ILoggingEvent event) {
LogContext context = LogContextContextHolder.get();
if(context instanceof LogWebSocketContext){
LogWebSocketContext socketContext = (LogWebSocketContext) context;
Object[] argumentArray = event.getArgumentArray();
List<Object> arguments = argumentArray == null ? Collections.emptyList() : new ArrayList<>(Arrays.asList(argumentArray));
ThrowableProxy throwableProxy = (ThrowableProxy) event.getThrowableProxy();
if(throwableProxy != null){
arguments.add(throwableProxy.getThrowable());
}
socketContext.log(new LogFmt(event.getLevel().levelStr.toLowerCase(),event.getFormattedMessage(),arguments));
}
}
}
websocket 接收启动信号,并输出相关日志,可以看到,关键的环节处的逻辑已经达到了剥离和格式化的目的
private static Logger logger = LoggerFactory.getLogger(Flower.class);
@OnMessage
public void onMessage(String message, Session session) {
JSONObject event = JSON.parseObject(message);
context = new LogWebSocketContext(session);
//执行主要逻辑代码,相关在该线程下执行的日志会得以记录输出
}
补充
本着配套送佛送到西的态度,nginx websocket配置支持如下
$connection_upgrade
map $http_upgrade $connection_upgrade {
default keep-alive; #默认为keep-alive 可以支持 一般http请求
'websocket' upgrade; #如果为websocket 则为 upgrade 可升级的。
}
server {
listen 80;
location /ws {
proxy_pass http://127.0.0.1:81/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; #此处配置 上面定义的变量
proxy_set_header Connection $connection_upgrade;
}
location /rule {
alias /usr/local/web/;
index index index.html index.htm;
}
}
总结
- 其实之前找过很多类似的组件及前后端联动思路,都不甚理想,没有让人眼前一亮的感觉,直到找到了以上相关的显示组件及后端的结合思路。
- 这期间耗费了比较多的时间去选择相关的组件及思路,借用一句用户常常折腾程序猿的话 “我不知道我想要什么,但你做出来了之后我就知道我不想要什么了”,试用结合过,才知道契合不契合。
- 本来老早之前就要分享,奈何身心俱受创伤,还是慢疗无效那种,最近强制调节得以喘息,以待后续分享吧。