Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b8b90af04 | |||
| 690e9288b8 | |||
| 2d93f70704 | |||
| 9f74a467ba | |||
| 4b36e35ace | |||
| 40f07bdd6e | |||
| 71ca306336 | |||
| 5274830cc6 |
4
pom.xml
4
pom.xml
@@ -10,7 +10,7 @@
|
||||
</parent>
|
||||
<groupId>cn.somkit</groupId>
|
||||
<artifactId>fmt</artifactId>
|
||||
<version>2.1.0</version>
|
||||
<version>2.1.3</version>
|
||||
<name>fmt</name>
|
||||
<description>File Manage System for by SpringBoot</description>
|
||||
<properties>
|
||||
@@ -55,7 +55,7 @@
|
||||
<dependency>
|
||||
<groupId>cn.metona</groupId>
|
||||
<artifactId>metona-mq-mini-pro</artifactId>
|
||||
<version>1.0.2</version>
|
||||
<version>2.0.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package cn.somkit.fmt;
|
||||
|
||||
import cn.metona.mq.util.MetonaMQUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
@@ -13,11 +12,6 @@ public class FmtApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(FmtApplication.class, args);
|
||||
|
||||
if(!MetonaMQUtil.isInitialized()){
|
||||
logger.info("Metona MQ Mini Pro 初始化...");
|
||||
MetonaMQUtil.init();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package cn.somkit.fmt.action;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.somkit.fmt.utils.OsInfoUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -31,6 +34,8 @@ import java.util.zip.ZipOutputStream;
|
||||
@RequestMapping("/download")
|
||||
public class DownloadAction {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
@Value("${somkit.upload.path.windows}")
|
||||
private String windows_path;
|
||||
|
||||
@@ -41,6 +46,7 @@ public class DownloadAction {
|
||||
public ModelAndView index(String keyboard) throws Exception{
|
||||
String path = OsInfoUtil.isWindows() ? windows_path :
|
||||
OsInfoUtil.isLinux() ? linux_path : null;
|
||||
logger.info("获取文件存储路径:{}", path);
|
||||
assert path != null;
|
||||
File folder = new File(path);
|
||||
File[] listOfFiles = folder.listFiles();
|
||||
@@ -59,6 +65,7 @@ public class DownloadAction {
|
||||
list = list.stream().filter(map -> String.valueOf(map.get("filename"))
|
||||
.contains(keyboard)).collect(Collectors.toList());
|
||||
}
|
||||
logger.info("关键词:{},文件数量:{}", keyboard, list.size());
|
||||
ModelAndView mv = new ModelAndView();
|
||||
mv.setViewName("download");
|
||||
mv.addObject("files", list);
|
||||
@@ -66,7 +73,7 @@ public class DownloadAction {
|
||||
return mv;
|
||||
}
|
||||
|
||||
private static Map<String, Object> getObjectMap(File file) {
|
||||
private Map<String, Object> getObjectMap(File file) {
|
||||
BigDecimal filesize = BigDecimal.valueOf(file.length())
|
||||
.divide(BigDecimal.valueOf(1024 * 1024), 2, RoundingMode.HALF_UP);
|
||||
Long time = file.lastModified();
|
||||
@@ -77,6 +84,7 @@ public class DownloadAction {
|
||||
map.put("filepath", file.getAbsolutePath());
|
||||
map.put("filesize", filesize);
|
||||
map.put("filetime", filetime);
|
||||
logger.info("文件信息:{}", JSONUtil.toJsonStr(map));
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -102,6 +110,7 @@ public class DownloadAction {
|
||||
}
|
||||
outputStream.flush();
|
||||
} catch (IOException e) {
|
||||
logger.error("IO错误:", e);
|
||||
throw new RuntimeException("IO错误: " + e.getMessage());
|
||||
} finally {
|
||||
// 尝试在流关闭后删除文件
|
||||
@@ -111,7 +120,7 @@ public class DownloadAction {
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// 记录日志或采取其他措施
|
||||
System.err.println("删除文件失败: " + e.getMessage());
|
||||
logger.error("删除文件失败:{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,38 +1,15 @@
|
||||
package cn.somkit.fmt.action;
|
||||
|
||||
import cn.metona.cache.Cache;
|
||||
import cn.metona.mq.util.MetonaMQUtil;
|
||||
import cn.somkit.fmt.socket.WebSocketServerHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/logging")
|
||||
public class LoggingAction {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
@Autowired
|
||||
private Cache<String, String> cache;
|
||||
|
||||
@GetMapping("/index")
|
||||
public String index() throws Exception{
|
||||
return "logging";
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@PostMapping("/close")
|
||||
public void close(Boolean closed) throws Exception {
|
||||
cache.put("closed", String.valueOf(closed));
|
||||
logger.info("Metona MQ Mini Pro 停止消费者(顺序消息)...");
|
||||
if(closed){
|
||||
MetonaMQUtil.stopOrderedConsuming();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
src/main/java/cn/somkit/fmt/config/LogMonitorAppender.java
Normal file
35
src/main/java/cn/somkit/fmt/config/LogMonitorAppender.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package cn.somkit.fmt.config;
|
||||
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.UnsynchronizedAppenderBase;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.metona.mq.MQToolkit;
|
||||
import cn.metona.mq.core.MessageQueue;
|
||||
import cn.somkit.fmt.entity.LoggerMessage;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
|
||||
public class LogMonitorAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
|
||||
|
||||
private static final String Queue_Name = "log-monitor";
|
||||
|
||||
@Override
|
||||
protected void append(ILoggingEvent event) {
|
||||
LoggerMessage msg = new LoggerMessage(
|
||||
event.getLevel().toString(),
|
||||
event.getThreadName(),
|
||||
event.getLoggerName(),
|
||||
event.getFormattedMessage(),
|
||||
DateUtil.format(Instant.ofEpochMilli(event.getTimeStamp()).atZone(ZoneId.systemDefault()).toLocalDateTime(),
|
||||
"yyyy-MM-dd HH:mm:ss.SSS"),
|
||||
ProcessHandle.current().pid()
|
||||
);
|
||||
//发送日志信息到日志监控队列
|
||||
MessageQueue.QueueStats queueStats = MQToolkit.getQueueStats(Queue_Name);
|
||||
if(queueStats != null && queueStats.isRunning() && !queueStats.isPaused()){
|
||||
MQToolkit.sendMessage(Queue_Name, "log.monitor", JSONUtil.toJsonStr(msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/main/java/cn/somkit/fmt/config/LogMonitorConfig.java
Normal file
50
src/main/java/cn/somkit/fmt/config/LogMonitorConfig.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package cn.somkit.fmt.config;
|
||||
|
||||
import cn.metona.mq.MQToolkit;
|
||||
import cn.metona.mq.consumer.MessageConsumer;
|
||||
import cn.metona.mq.utils.MonitorUtils;
|
||||
import cn.somkit.fmt.socket.SocketManage;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class LogMonitorConfig {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
private static final String Queue_Name = "log-monitor";
|
||||
|
||||
private static final String Consumer_Id = "log-monitor-consumer";
|
||||
|
||||
@PostConstruct
|
||||
public void run(){
|
||||
//创建日志监控队列
|
||||
MQToolkit.createQueue(Queue_Name, Boolean.FALSE);
|
||||
//创建日志监控消费者
|
||||
MessageConsumer consumer = MQToolkit.createConsumer(Consumer_Id, Queue_Name, message -> {
|
||||
List<WebSocketSession> list = SocketManage.all();
|
||||
if(list != null && !list.isEmpty()){
|
||||
list.forEach(session -> {
|
||||
if(session.isOpen()){
|
||||
try {
|
||||
session.sendMessage(new TextMessage(message.body()));
|
||||
} catch (IOException e) {
|
||||
logger.error("发送消息失败:", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
//启动消费者
|
||||
consumer.start();
|
||||
// 显示队列统计
|
||||
MonitorUtils.printQueueStats(Queue_Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.somkit.fmt.config;
|
||||
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.LoggerContext;
|
||||
import jakarta.servlet.ServletContextEvent;
|
||||
import jakarta.servlet.ServletContextListener;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class LogbackConfigListener implements ServletContextListener {
|
||||
|
||||
@Override
|
||||
public void contextInitialized(ServletContextEvent event) {
|
||||
// 配置logback
|
||||
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
|
||||
Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
|
||||
|
||||
// 添加自定义appender
|
||||
LogMonitorAppender appender = new LogMonitorAppender();
|
||||
appender.setContext(loggerContext);
|
||||
appender.start();
|
||||
rootLogger.addAppender(appender);
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,19 @@ package cn.somkit.fmt.entity;
|
||||
|
||||
public class LoggerMessage {
|
||||
String level;
|
||||
String threadName;
|
||||
String name;
|
||||
String message;
|
||||
String timestamp;
|
||||
long number;
|
||||
|
||||
public LoggerMessage(String level, String loggerName, String message, String timestamp) {
|
||||
public LoggerMessage(String level, String threadName, String loggerName, String message, String timestamp, long number) {
|
||||
this.level = level;
|
||||
this.threadName = threadName;
|
||||
this.name = loggerName;
|
||||
this.message = message;
|
||||
this.timestamp = timestamp;
|
||||
this.number = number;
|
||||
}
|
||||
|
||||
public String getLevel() {
|
||||
@@ -21,6 +25,14 @@ public class LoggerMessage {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public String getThreadName() {
|
||||
return threadName;
|
||||
}
|
||||
|
||||
public void setThreadName(String threadName) {
|
||||
this.threadName = threadName;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
@@ -44,4 +56,12 @@ public class LoggerMessage {
|
||||
public void setTimestamp(String timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public long getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public void setNumber(long number) {
|
||||
this.number = number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package cn.somkit.fmt.filter;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.filter.Filter;
|
||||
import ch.qos.logback.core.spi.FilterReply;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.metona.mq.exception.MessageSendException;
|
||||
import cn.metona.mq.util.MetonaMQUtil;
|
||||
import cn.somkit.fmt.entity.LoggerMessage;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
|
||||
public class LogStashFilter extends Filter<ILoggingEvent> {
|
||||
|
||||
Level level;
|
||||
|
||||
public LogStashFilter() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterReply decide(ILoggingEvent e) {
|
||||
LoggerMessage msg = new LoggerMessage(
|
||||
e.getLevel().toString(),
|
||||
e.getLoggerName(),
|
||||
e.getFormattedMessage(),
|
||||
DateUtil.format(Instant.ofEpochMilli(e.getTimeStamp()).atZone(ZoneId.systemDefault()).toLocalDateTime(),
|
||||
"yyyy-MM-dd HH:mm:ss.SSS")
|
||||
);
|
||||
try {
|
||||
if(MetonaMQUtil.isInitialized() && MetonaMQUtil.isOrderedConsumerRunning()){
|
||||
MetonaMQUtil.send("log-topic", "log-monitor", JSONUtil.toJsonStr(msg));
|
||||
}
|
||||
} catch (MessageSendException ex) {
|
||||
System.out.println("发送消息队列失败");
|
||||
}
|
||||
return FilterReply.NEUTRAL;
|
||||
}
|
||||
|
||||
public void setLevel(String level) {
|
||||
this.level = Level.toLevel(level);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (this.level != null) {
|
||||
super.start();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
31
src/main/java/cn/somkit/fmt/socket/SocketManage.java
Normal file
31
src/main/java/cn/somkit/fmt/socket/SocketManage.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package cn.somkit.fmt.socket;
|
||||
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class SocketManage {
|
||||
|
||||
private SocketManage() {
|
||||
}
|
||||
|
||||
private static final Map<String, WebSocketSession> onlineMap = new ConcurrentHashMap<>();
|
||||
|
||||
public static void put(WebSocketSession session){
|
||||
onlineMap.put(session.getId(), session);
|
||||
}
|
||||
|
||||
public static WebSocketSession get(String sessionId){
|
||||
return onlineMap.get(sessionId);
|
||||
}
|
||||
|
||||
public static void remove(String sessionId){
|
||||
onlineMap.remove(sessionId);
|
||||
}
|
||||
|
||||
public static List<WebSocketSession> all(){
|
||||
return onlineMap.values().stream().toList();
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,29 @@
|
||||
package cn.somkit.fmt.socket;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.metona.cache.Cache;
|
||||
import cn.metona.mq.consumer.MessageListener;
|
||||
import cn.metona.mq.core.Message;
|
||||
import cn.metona.mq.util.MetonaMQUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
public class WebSocketServerHandler implements WebSocketHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
@Autowired
|
||||
private Cache<String, String> cache;
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||
push(session);
|
||||
}
|
||||
|
||||
private void push(WebSocketSession session) throws IOException {
|
||||
boolean closed = StrUtil.isNotBlank(cache.get("closed")) && Boolean.parseBoolean(cache.get("closed"));
|
||||
if(!closed){
|
||||
try {
|
||||
if(MetonaMQUtil.isInitialized() && !MetonaMQUtil.isOrderedConsumerRunning()){
|
||||
logger.info("Metona MQ Mini Pro 订阅主题(log-topic)(顺序消息)...");
|
||||
MetonaMQUtil.subscribeOrdered("log-topic", new MessageListener() {
|
||||
@Override
|
||||
public void onMessage(Message message) {
|
||||
try {
|
||||
session.sendMessage(new TextMessage(message.getBody()));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logger.info("Metona MQ Mini Pro 启动消费者(顺序消息)...");
|
||||
MetonaMQUtil.startOrderedConsuming();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Metona MQ Mini Pro 异常", e);
|
||||
}
|
||||
}
|
||||
SocketManage.put(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
|
||||
push(session);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
|
||||
|
||||
SocketManage.remove(session.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
|
||||
|
||||
SocketManage.remove(session.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package cn.somkit.fmt.utils;
|
||||
|
||||
import cn.somkit.fmt.entity.LoggerMessage;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
public final class LoggerQueue {
|
||||
private static final LoggerQueue INSTANCE = new LoggerQueue();
|
||||
private final BlockingQueue<LoggerMessage> queue = new LinkedBlockingQueue<>(100000);
|
||||
|
||||
public static LoggerQueue getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public void push(LoggerMessage msg) {
|
||||
queue.offer(msg); // 非阻塞插入
|
||||
}
|
||||
|
||||
public LoggerMessage poll() {
|
||||
try {
|
||||
return queue.take(); // 阻塞直到有数据
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
<!-- 日志输出到控制台 -->
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<filter class="cn.somkit.fmt.filter.LogStashFilter">
|
||||
<level>DEBUG</level>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>INFO</level>
|
||||
</filter>
|
||||
<encoder>
|
||||
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||
@@ -15,7 +15,7 @@
|
||||
</appender>
|
||||
|
||||
<!-- 指定日志输出级别,以及启动的Appender -->
|
||||
<root level="DEBUG">
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
</configuration>
|
||||
|
||||
@@ -14,18 +14,6 @@
|
||||
|
||||
let ws = null;
|
||||
|
||||
let logMonitor = async (closed = false) => {
|
||||
const options = {
|
||||
url: Fmt.ctx() + '/logging/close',
|
||||
data: {closed: closed},
|
||||
method: 'post'
|
||||
};
|
||||
await Fmt.axios(options).then(() => {}).catch((err) => console.error(err));
|
||||
if(ws){
|
||||
ws.send('发送日志');
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new LogMonitorAdaptive('#logContainer', {
|
||||
theme: 'dark',
|
||||
maxLines: 10000,
|
||||
@@ -43,8 +31,8 @@
|
||||
showLevel: true, // 是否显示日志级别标签
|
||||
wordWrap: true, // 日志内容是否自动换行(true=换行,false=横向滚动)
|
||||
//暂停/继续 回调函数
|
||||
onTogglePause: async (isPaused) => {
|
||||
await logMonitor(isPaused);
|
||||
onTogglePause: (isPaused) => {
|
||||
|
||||
},
|
||||
onCreated: () => {
|
||||
console.log('日志容器已创建');
|
||||
@@ -59,14 +47,11 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
ws.onopen = function () {
|
||||
|
||||
}
|
||||
|
||||
ws.onmessage = function (event) {
|
||||
if(event.data){
|
||||
let data = JSON.parse(event.data);
|
||||
logger.log(data.name + ' : ' + data.message, data.level, data.timestamp);
|
||||
let message = `${data.number} --- [ ${data.threadName} ] <span style="color: #198cff;">${data.name}</span> : ${data.message}`;
|
||||
logger.log(message, data.level, data.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,3 +33,17 @@
|
||||
```
|
||||
引入metona-mq-mini-pro消息队列,重构实时日志获取方式
|
||||
```
|
||||
> v2.1.1
|
||||
```
|
||||
升级metona-mq-mini-pro到2.0.0,重构实时日志获取方式
|
||||
```
|
||||
> v2.1.2
|
||||
```
|
||||
升级metona-mq-mini-pro到2.0.1
|
||||
```
|
||||
> v2.1.3
|
||||
```
|
||||
使用ServletContextListener方式获取实时日志
|
||||
页面日志要素添加进程id和线程名称,优化样式
|
||||
日志打印级别调整为INFO
|
||||
```
|
||||
Reference in New Issue
Block a user