个人技术网站 欢迎关注
网上有很多后台推送日志到前端页面的例子,这里我也借鉴了别人的做法 稍加改进一下。以前做前端页面显示日志一般都会想到ajax轮询去做,这样太耗费服务器资源了,性能也很差。使用长连接来做更稳妥,这就想到了用websocket。刚好spring-boot对websocket支持非常不错,使用很方便,接下来开整。
简单的spring-boot工程这里就不做过多的讲解了,只叙述核心部分
首先导入依赖
org.springframework.boot
spring-boot-starter-websocket
首先在项目的日志配置文件中新增/修改此节点配置
${CONSOLE_LOG_PATTERN}
UTF-8
此节点配置主要是为了把控制台输出的日志交给我们自己的日志过滤器进行处理(此节点不要指定日志等级,记得
接下来实现自己的日志过滤器
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;
import com.xcloud.api.system.entity.LoggerMessage;
import org.springframework.stereotype.Service;
import java.text.DateFormat;
import java.util.Date;
/**
* Xcloud-Api By IDEA
* Created by LaoWang on 2018/8/25.
*/
@Service
public class LogFilter extends Filter {
@Override
public FilterReply decide(ILoggingEvent event) {
String exception = "";
IThrowableProxy iThrowableProxy1 = event.getThrowableProxy();
if(iThrowableProxy1!=null){
exception = ""+iThrowableProxy1.getClassName()+" "+iThrowableProxy1.getMessage()+"";
for(int i=0; i"+iThrowableProxy1.getStackTraceElementProxyArray()[i].toString()+"";
}
}
LoggerMessage loggerMessage = new LoggerMessage(
event.getMessage()
, DateFormat.getDateTimeInstance().format(new Date(event.getTimeStamp())),
event.getThreadName(),
event.getLoggerName(),
event.getLevel().levelStr,
exception,
""
);
LoggerQueue.getInstance().push(loggerMessage);
return FilterReply.ACCEPT;
}
}
这部分代码网上大部分都是一样的,但是并没有对异常具体信息进行处理,所以这里我把具体异常信息日志也添加进去了
创建一个阻塞队列,作为日志系统输出的日志的一个临时载体
import com.xcloud.api.system.entity.LoggerMessage;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class LoggerQueue {
//队列大小
public static final int QUEUE_MAX_SIZE = 10000;
private static LoggerQueue alarmMessageQueue = new LoggerQueue();
//阻塞队列
private BlockingQueue blockingQueue = new LinkedBlockingQueue<>(QUEUE_MAX_SIZE);
private LoggerQueue() {
}
public static LoggerQueue getInstance() {
return alarmMessageQueue;
}
/**
* 消息入队
*
* @param log
* @return
*/
public boolean push(LoggerMessage log) {
return this.blockingQueue.add(log);//队列满了就抛出异常,不阻塞
}
/**
* 消息出队
*
* @return
*/
public LoggerMessage poll() {
LoggerMessage result = null;
try {
result = (LoggerMessage) this.blockingQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
return result;
}
}
创建一个日志实体(这里我使用了lombok,如果没用的话,请自行生成get,set和全参数构造方法)
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* 日志消息实体
*/
@Getter
@Setter
@ToString
@AllArgsConstructor
public class LoggerMessage {
private String body;
private String timestamp;
private String threadName;
private String className;
private String level;
private String exception;
private String cause;
}
接下来配置WebSocket
import com.xcloud.api.system.core.LoggerQueue;
import com.xcloud.api.system.entity.LoggerMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import javax.annotation.PostConstruct;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Xcloud-Api By IDEA
* 配置WebSocket消息代理端点,即stomp服务端
* 为了连接安全,setAllowedOrigins设置的允许连接的源地址
* 如果在非这个配置的地址下发起连接会报403
* 进一步还可以使用addInterceptors设置拦截器,来做相关的鉴权操作
* Created by LaoWang on 2018/8/25.
*/
@Slf4j
@Configuration
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket")
.setAllowedOrigins("*")
.withSockJS();
}
/**
* 推送日志到/topic/pullLogger
*/
@PostConstruct
public void pushLogger(){
ExecutorService executorService= Executors.newFixedThreadPool(2);
Runnable runnable=new Runnable() {
@Override
public void run() {
while (true) {
try {
LoggerMessage log = LoggerQueue.getInstance().poll();
if(log!=null){
if(messagingTemplate!=null)
messagingTemplate.convertAndSend("/topic/pullLogger",log);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
executorService.submit(runnable);
}
}
如果项目配置有拦截器获取其他权限控制的,请开放/websocket
前端页面:这里我使用了原生swagger+layer,请自行根据自己页面进行配置
配置一个按钮点击弹出layer弹窗,日志颜色,具体的一些样式可根据自己喜好选择
$(".log").click(function () {
//iframe层
layer.open({
type: 1,
title: '接口实时日志',
shadeClose: false,
shade: 0.7,
maxmin: true,
area: ['80%', '70%'],
content: $("#logdiv").html(), //iframe的url
cancel: function(index){
closeSocket();
}
});
});
var stompClient = null;
function openSocket() {
if (stompClient == null) {
if($("#log-container").find("span").length==0){
$("#log-container div").after("通道连接成功,静默等待.....");
}
var socket = new SockJS('websocket?token=kl');
stompClient = Stomp.over(socket);
stompClient.connect({token: "kl"}, function (frame) {
stompClient.subscribe('/topic/pullLogger', function (event) {
var content = JSON.parse(event.body);
var leverhtml = '';
var className = '' + content.className + '';
switch (content.level) {
case 'INFO':
leverhtml = '' + content.level + '';
break;
case 'DEBUG':
leverhtml = '' + content.level + '';
break;
case 'WARN':
leverhtml = '' + content.level + '';
break;
case 'ERROR':
leverhtml = '' + content.level + '';
break;
}
$("#log-container div").append("" + content.timestamp + " " + leverhtml + " --- [" + content.threadName + "] " + className + " :" + content.body + "
");
if (content.exception != "") {
$("#log-container div").append("" + content.exception + "
");
}
if (content.cause != "") {
$("#log-container div").append("" + content.cause + "
");
}
$("#log-container").scrollTop($("#log-container div").height() - $("#log-container").height());
}, {
token: "kltoen"
});
});
}
}
function closeSocket() {
if (stompClient != null) {
stompClient.disconnect();
stompClient = null;
}
}
最后来一个效果图(图中日志等级动态配置将在下篇博客说明)有什么不对的地方欢迎大牛们评论!!!