Merge remote-tracking branch 'origin/master' into develop
# Conflicts: # src/main/java/cn/somkit/fmt/action/DownloadAction.java # src/main/java/cn/somkit/fmt/action/UploadAction.java # src/main/java/cn/somkit/fmt/config/LogSocketConfig.java # src/main/java/cn/somkit/fmt/config/RocksDBConfig.java # src/main/java/cn/somkit/fmt/utils/LogSocketUtils.java # src/main/java/cn/somkit/fmt/utils/RocksDBUtils.java # src/main/resources/application.yml # src/main/resources/templates/logging.html
This commit is contained in:
38
pom.xml
38
pom.xml
@@ -5,7 +5,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.1.1</version>
|
<version>3.5.3</version>
|
||||||
<relativePath/> <!-- lookup parent from repository -->
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>cn.somkit</groupId>
|
<groupId>cn.somkit</groupId>
|
||||||
@@ -14,44 +14,42 @@
|
|||||||
<name>fmt</name>
|
<name>fmt</name>
|
||||||
<description>fmt</description>
|
<description>fmt</description>
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>17</java.version>
|
<java.version>21</java.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>metona-maven</id>
|
||||||
|
<url>https://gitee.com/thzxx/maven/raw/master</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.aspectj</groupId>
|
|
||||||
<artifactId>aspectjweaver</artifactId>
|
|
||||||
<version>1.9.7</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.rocksdb</groupId>
|
|
||||||
<artifactId>rocksdbjni</artifactId>
|
|
||||||
<version>8.3.2</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-json</artifactId>
|
<artifactId>hutool-json</artifactId>
|
||||||
<version>5.8.21</version>
|
<version>5.8.25</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.metona</groupId>
|
||||||
|
<artifactId>metona-cache-spring-boot-starter</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
package cn.somkit.fmt.action;
|
package cn.somkit.fmt.action;
|
||||||
|
|
||||||
import cn.somkit.fmt.annotation.ApiOperate;
|
import cn.somkit.fmt.utils.OsInfoUtil;
|
||||||
import cn.somkit.fmt.config.FmtConfig;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import cn.somkit.fmt.config.RocksDBConfig;
|
|
||||||
import cn.somkit.fmt.utils.MD5Utils;
|
|
||||||
import cn.somkit.fmt.utils.ParamUtils;
|
|
||||||
import cn.somkit.fmt.utils.PathUtils;
|
|
||||||
import cn.somkit.fmt.utils.RocksDBUtils;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@@ -37,54 +31,29 @@ import java.util.zip.ZipOutputStream;
|
|||||||
@RequestMapping("/download")
|
@RequestMapping("/download")
|
||||||
public class DownloadAction {
|
public class DownloadAction {
|
||||||
|
|
||||||
|
@Value("${somkit.upload.path.windows}")
|
||||||
|
private String windows_path;
|
||||||
|
|
||||||
|
@Value("${somkit.upload.path.linux}")
|
||||||
|
private String linux_path;
|
||||||
|
|
||||||
@GetMapping("/index")
|
@GetMapping("/index")
|
||||||
public ModelAndView index(String keyboard) throws Exception{
|
public ModelAndView index(String keyboard) throws Exception{
|
||||||
PathUtils.directory(ParamUtils.getUploadPath());
|
String path = OsInfoUtil.isWindows() ? windows_path :
|
||||||
File folder = new File(ParamUtils.getUploadPath());
|
OsInfoUtil.isLinux() ? linux_path : null;
|
||||||
|
assert path != null;
|
||||||
|
File folder = new File(path);
|
||||||
File[] listOfFiles = folder.listFiles();
|
File[] listOfFiles = folder.listFiles();
|
||||||
List<Map<String, Object>> list = new ArrayList<>();
|
List<Map<String, Object>> list = new ArrayList<>();
|
||||||
List<String> keys = RocksDBUtils.getAllKey(RocksDBConfig.RocksDB_Column_Family);
|
|
||||||
if(listOfFiles != null){
|
if(listOfFiles != null){
|
||||||
for (File file : listOfFiles) {
|
for (File file : listOfFiles) {
|
||||||
if(!file.isFile()){
|
if(!file.isFile()){
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if(!keys.contains(file.getName())){
|
Map<String, Object> map = getObjectMap(file);
|
||||||
try (FileInputStream is = new FileInputStream(file)) {
|
|
||||||
String hash = MD5Utils.md5HashCode(is);
|
|
||||||
RocksDBUtils.put(RocksDBConfig.RocksDB_Column_Family, file.getName(), hash);
|
|
||||||
}catch (Exception e){e.printStackTrace();}
|
|
||||||
}
|
|
||||||
BigDecimal filesize = BigDecimal.valueOf(file.length())
|
|
||||||
.divide(BigDecimal.valueOf(1024 * 1024), 2, RoundingMode.HALF_UP);
|
|
||||||
Long time = file.lastModified();
|
|
||||||
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
|
||||||
String filetime = format.format(time);
|
|
||||||
Map<String, Object> map = new HashMap<>();
|
|
||||||
map.put("filename", file.getName());
|
|
||||||
map.put("filepath", file.getAbsolutePath());
|
|
||||||
map.put("filesize", filesize);
|
|
||||||
map.put("filetime", filetime);
|
|
||||||
list.add(map);
|
list.add(map);
|
||||||
}
|
}
|
||||||
List<String> filenames = list.stream().map(map -> String.valueOf(map.get("filename"))).toList();
|
|
||||||
keys.forEach(key -> {
|
|
||||||
try {
|
|
||||||
if(!filenames.contains(key)){
|
|
||||||
RocksDBUtils.delete(RocksDBConfig.RocksDB_Column_Family, key);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}else{
|
|
||||||
keys.forEach(key -> {
|
|
||||||
try {
|
|
||||||
RocksDBUtils.delete(RocksDBConfig.RocksDB_Column_Family, key);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if(StringUtils.hasText(keyboard) && !CollectionUtils.isEmpty(list)){
|
if(StringUtils.hasText(keyboard) && !CollectionUtils.isEmpty(list)){
|
||||||
list = list.stream().filter(map -> String.valueOf(map.get("filename"))
|
list = list.stream().filter(map -> String.valueOf(map.get("filename"))
|
||||||
@@ -97,7 +66,20 @@ public class DownloadAction {
|
|||||||
return mv;
|
return mv;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperate(description = "StreamingResponseBody方式下载文件")
|
private static 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();
|
||||||
|
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
String filetime = format.format(time);
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("filename", file.getName());
|
||||||
|
map.put("filepath", file.getAbsolutePath());
|
||||||
|
map.put("filesize", filesize);
|
||||||
|
map.put("filetime", filetime);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping(value = "/file", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
@GetMapping(value = "/file", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||||
public DeferredResult<ResponseEntity<StreamingResponseBody>> downloadFileByPath(String path, Boolean temp) throws Exception {
|
public DeferredResult<ResponseEntity<StreamingResponseBody>> downloadFileByPath(String path, Boolean temp) throws Exception {
|
||||||
final File file = new File(path);
|
final File file = new File(path);
|
||||||
@@ -140,15 +122,20 @@ public class DownloadAction {
|
|||||||
|
|
||||||
@PostMapping("/packZip")
|
@PostMapping("/packZip")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
@ApiOperate(description = "批量文件打包下载")
|
|
||||||
public Map<String, Object> packZip(String filenames) throws Exception{
|
public Map<String, Object> packZip(String filenames) throws Exception{
|
||||||
try {
|
try {
|
||||||
PathUtils.directory(ParamUtils.getUploadPath());
|
String path = OsInfoUtil.isWindows() ? windows_path :
|
||||||
PathUtils.directory(ParamUtils.getTempFilePath());
|
OsInfoUtil.isLinux() ? linux_path : null;
|
||||||
|
assert path != null;
|
||||||
|
//临时文件目录
|
||||||
|
String temp = "temp";
|
||||||
|
String zipPath = path + File.separator + temp;
|
||||||
|
File zipFile = new File(zipPath);
|
||||||
|
if (!zipFile.exists()) zipFile.mkdirs();
|
||||||
String zipName = String.valueOf(System.currentTimeMillis());
|
String zipName = String.valueOf(System.currentTimeMillis());
|
||||||
String zipDir = ParamUtils.getTempFilePath() + File.separator + zipName + ".zip";
|
String zipDir = zipPath + File.separator + zipName + ".zip";
|
||||||
ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipDir));
|
ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipDir));
|
||||||
File folder = new File(ParamUtils.getUploadPath());
|
File folder = new File(path);
|
||||||
File[] listOfFiles = folder.listFiles();
|
File[] listOfFiles = folder.listFiles();
|
||||||
assert listOfFiles != null;
|
assert listOfFiles != null;
|
||||||
for (File file : listOfFiles) {
|
for (File file : listOfFiles) {
|
||||||
@@ -178,27 +165,25 @@ public class DownloadAction {
|
|||||||
|
|
||||||
@PostMapping("/delete")
|
@PostMapping("/delete")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
@ApiOperate(description = "单文件删除")
|
|
||||||
public String delete(String path) throws Exception{
|
public String delete(String path) throws Exception{
|
||||||
File file = new File(path);
|
File file = new File(path);
|
||||||
RocksDBUtils.delete(RocksDBConfig.RocksDB_Column_Family, file.getName());
|
|
||||||
FileSystemUtils.deleteRecursively(file);
|
FileSystemUtils.deleteRecursively(file);
|
||||||
return "删除成功";
|
return "删除成功";
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/batchDel")
|
@PostMapping("/batchDel")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
@ApiOperate(description = "批量文件删除")
|
|
||||||
public String batchDel(String filenames) throws Exception{
|
public String batchDel(String filenames) throws Exception{
|
||||||
PathUtils.directory(ParamUtils.getUploadPath());
|
String path = OsInfoUtil.isWindows() ? windows_path :
|
||||||
File folder = new File(ParamUtils.getUploadPath());
|
OsInfoUtil.isLinux() ? linux_path : null;
|
||||||
|
assert path != null;
|
||||||
|
File folder = new File(path);
|
||||||
File[] listOfFiles = folder.listFiles();
|
File[] listOfFiles = folder.listFiles();
|
||||||
assert listOfFiles != null;
|
assert listOfFiles != null;
|
||||||
for (File file : listOfFiles) {
|
for (File file : listOfFiles) {
|
||||||
if(!Arrays.asList(filenames.split(",")).contains(file.getName())){
|
if(!Arrays.asList(filenames.split(",")).contains(file.getName())){
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
RocksDBUtils.delete(RocksDBConfig.RocksDB_Column_Family, file.getName());
|
|
||||||
FileSystemUtils.deleteRecursively(file);
|
FileSystemUtils.deleteRecursively(file);
|
||||||
}
|
}
|
||||||
return "删除成功";
|
return "删除成功";
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package cn.somkit.fmt.action;
|
package cn.somkit.fmt.action;
|
||||||
|
|
||||||
import cn.somkit.fmt.annotation.ApiOperate;
|
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|||||||
@@ -1,16 +1,37 @@
|
|||||||
package cn.somkit.fmt.action;
|
package cn.somkit.fmt.action;
|
||||||
|
|
||||||
import cn.somkit.fmt.annotation.ApiOperate;
|
import cn.metona.cache.Cache;
|
||||||
|
import cn.somkit.fmt.entity.LoggerMessage;
|
||||||
|
import cn.somkit.fmt.utils.LoggerQueue;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
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.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/logging")
|
@RequestMapping("/logging")
|
||||||
public class LoggingAction {
|
public class LoggingAction {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Cache<String, Object> cache;
|
||||||
|
|
||||||
@GetMapping("/index")
|
@GetMapping("/index")
|
||||||
public String index() throws Exception{
|
public String index() throws Exception{
|
||||||
return "logging";
|
return "logging";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
@PostMapping("/close")
|
||||||
|
public void close(Boolean closed) throws Exception {
|
||||||
|
cache.put("closed", closed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
package cn.somkit.fmt.action;
|
package cn.somkit.fmt.action;
|
||||||
|
|
||||||
import cn.somkit.fmt.annotation.ApiOperate;
|
import cn.somkit.fmt.utils.OsInfoUtil;
|
||||||
import cn.somkit.fmt.config.FmtConfig;
|
|
||||||
import cn.somkit.fmt.config.RocksDBConfig;
|
|
||||||
import cn.somkit.fmt.utils.MD5Utils;
|
|
||||||
import cn.somkit.fmt.utils.ParamUtils;
|
|
||||||
import cn.somkit.fmt.utils.PathUtils;
|
|
||||||
import cn.somkit.fmt.utils.RocksDBUtils;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -24,6 +18,12 @@ import java.util.*;
|
|||||||
@RequestMapping("/upload")
|
@RequestMapping("/upload")
|
||||||
public class UploadAction {
|
public class UploadAction {
|
||||||
|
|
||||||
|
@Value("${somkit.upload.path.windows}")
|
||||||
|
private String windows_path;
|
||||||
|
|
||||||
|
@Value("${somkit.upload.path.linux}")
|
||||||
|
private String linux_path;
|
||||||
|
|
||||||
@GetMapping("/index")
|
@GetMapping("/index")
|
||||||
public String index() throws Exception{
|
public String index() throws Exception{
|
||||||
return "upload";
|
return "upload";
|
||||||
@@ -31,16 +31,14 @@ public class UploadAction {
|
|||||||
|
|
||||||
@PostMapping("/execute")
|
@PostMapping("/execute")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
@ApiOperate(description = "文件上传")
|
|
||||||
public Map<String, Object> execute(HttpServletRequest request) throws Exception{
|
public Map<String, Object> execute(HttpServletRequest request) throws Exception{
|
||||||
//多个文件上传 就只是简单的多文件上传保存在本地的磁盘
|
//多个文件上传 就只是简单的多文件上传保存在本地的磁盘
|
||||||
if (request instanceof MultipartHttpServletRequest mrequest) {
|
if (request instanceof MultipartHttpServletRequest mrequest) {
|
||||||
PathUtils.directory(ParamUtils.getUploadPath());
|
|
||||||
List<MultipartFile> files = mrequest.getFiles("file");
|
List<MultipartFile> files = mrequest.getFiles("file");
|
||||||
// 取出每一个上传文件
|
// 取出每一个上传文件
|
||||||
for (MultipartFile file : files) {
|
for (MultipartFile file : files) {
|
||||||
try {
|
try {
|
||||||
this.saveFile(file);// 保存上传信息
|
this.saveFile(file); // 保存上传信息
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
@@ -58,30 +56,14 @@ public class UploadAction {
|
|||||||
* @throws Exception 上传异常
|
* @throws Exception 上传异常
|
||||||
*/
|
*/
|
||||||
public void saveFile(MultipartFile file) throws Exception {
|
public void saveFile(MultipartFile file) throws Exception {
|
||||||
|
String path = OsInfoUtil.isWindows() ? windows_path :
|
||||||
|
OsInfoUtil.isLinux() ? linux_path : null;
|
||||||
|
assert path != null;
|
||||||
String filePath = "";
|
String filePath = "";
|
||||||
if (file != null && file.getSize() > 0) {// 有文件上传
|
if (file != null && file.getSize() > 0) { // 有文件上传
|
||||||
String fileName = verify(file, null);// 创建文件名称
|
filePath = path + File.separator + file.getOriginalFilename();
|
||||||
if(StringUtils.hasText(fileName)){
|
File saveFile = new File(filePath) ;
|
||||||
String hash = MD5Utils.md5HashCode(file.getInputStream());
|
file.transferTo(saveFile); // 文件保存
|
||||||
RocksDBUtils.put(RocksDBConfig.RocksDB_Column_Family, fileName, hash);
|
|
||||||
filePath = ParamUtils.getUploadPath() + File.separator + fileName;
|
|
||||||
File saveFile = new File(filePath) ;
|
|
||||||
file.transferTo(saveFile); // 文件保存
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String verify(MultipartFile file, String filename) throws Exception {
|
|
||||||
String key = StringUtils.hasText(filename) ? filename : Objects.requireNonNull(file.getOriginalFilename());
|
|
||||||
String hash = RocksDBUtils.get(RocksDBConfig.RocksDB_Column_Family, key);
|
|
||||||
if(!StringUtils.hasText(hash)){
|
|
||||||
return StringUtils.hasText(filename) ? filename : file.getOriginalFilename();
|
|
||||||
}
|
|
||||||
String newHash = MD5Utils.md5HashCode(file.getInputStream());
|
|
||||||
if(!hash.equals(newHash)){
|
|
||||||
String newFilename = "(1)" + file.getOriginalFilename();
|
|
||||||
return verify(file, newFilename);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package cn.somkit.fmt.annotation;
|
|
||||||
|
|
||||||
import java.lang.annotation.*;
|
|
||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Target({ElementType.METHOD})
|
|
||||||
@Documented
|
|
||||||
public @interface ApiOperate {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 描述
|
|
||||||
*/
|
|
||||||
String description() default "";
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package cn.somkit.fmt.annotation.aspect;
|
|
||||||
|
|
||||||
import cn.hutool.json.JSONUtil;
|
|
||||||
import cn.somkit.fmt.annotation.ApiOperate;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
|
||||||
import org.aspectj.lang.annotation.Around;
|
|
||||||
import org.aspectj.lang.annotation.Aspect;
|
|
||||||
import org.aspectj.lang.annotation.Pointcut;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Aspect
|
|
||||||
@Component
|
|
||||||
public class ApiOperateAspect {
|
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
|
||||||
|
|
||||||
/** 换行符 */
|
|
||||||
private static final String LINE_SEPARATOR = System.lineSeparator();
|
|
||||||
|
|
||||||
/** 以自定义 @ApiOperate 注解为切点 */
|
|
||||||
@Pointcut("@annotation(apiOperate)")
|
|
||||||
public void ApiOperate(ApiOperate apiOperate) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 环绕注入
|
|
||||||
* @param joinPoint
|
|
||||||
* @param apiOperate
|
|
||||||
* @return
|
|
||||||
* @throws Throwable
|
|
||||||
*/
|
|
||||||
@Around(value = "ApiOperate(apiOperate)", argNames = "joinPoint,apiOperate")
|
|
||||||
public Object doAround(ProceedingJoinPoint joinPoint, ApiOperate apiOperate) throws Throwable {
|
|
||||||
long startTime = System.currentTimeMillis();
|
|
||||||
Object result = joinPoint.proceed();
|
|
||||||
|
|
||||||
// 开始打印请求日志
|
|
||||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
|
||||||
assert attributes != null;
|
|
||||||
HttpServletRequest request = attributes.getRequest();
|
|
||||||
|
|
||||||
long timeConsuming = System.currentTimeMillis() - startTime;
|
|
||||||
|
|
||||||
// 打印请求相关参数
|
|
||||||
logger.info("========================================== Start ==========================================");
|
|
||||||
// 打印请求 url
|
|
||||||
logger.info("URL : {}", request.getRequestURL().toString());
|
|
||||||
// 打印描述信息
|
|
||||||
logger.info("Description : {}", apiOperate.description());
|
|
||||||
// 打印 Http method
|
|
||||||
logger.info("HTTP Method : {}", request.getMethod());
|
|
||||||
// 打印调用 controller 的全路径以及执行方法
|
|
||||||
logger.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
|
|
||||||
// 打印请求的 IP
|
|
||||||
logger.info("IP : {}", request.getRemoteAddr());
|
|
||||||
// 打印请求Headers
|
|
||||||
logger.info("Header Args : {}", headers(request));
|
|
||||||
// 打印请求入参
|
|
||||||
logger.info("Request Args : {}", Arrays.toString(joinPoint.getArgs()));
|
|
||||||
// 打印出参
|
|
||||||
logger.info("Response Args : {}", JSONUtil.toJsonStr(result));
|
|
||||||
// 执行耗时
|
|
||||||
logger.info("Time-Consuming : {} ms", timeConsuming);
|
|
||||||
// 接口结束后换行,方便分割查看
|
|
||||||
logger.info("=========================================== End ===========================================" + LINE_SEPARATOR);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有header参数
|
|
||||||
* @param request
|
|
||||||
* @return Map<String, String> headers
|
|
||||||
*/
|
|
||||||
private Map<String, String> headers(HttpServletRequest request){
|
|
||||||
Map<String, String> headerMap = new HashMap<>();
|
|
||||||
Enumeration<String> enumeration = request.getHeaderNames();
|
|
||||||
while (enumeration.hasMoreElements()) {
|
|
||||||
String name = enumeration.nextElement();
|
|
||||||
String value = request.getHeader(name);
|
|
||||||
headerMap.put(name, value);
|
|
||||||
}
|
|
||||||
return headerMap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package cn.somkit.fmt.config;
|
|
||||||
|
|
||||||
import cn.somkit.fmt.utils.PathUtils;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class LogSocketConfig {
|
|
||||||
|
|
||||||
public static String Log_File_Path = "./logs/fmt-server.log";
|
|
||||||
|
|
||||||
public static String Max_Read_Length = "500";
|
|
||||||
|
|
||||||
public static String Read_Interval = "1000";
|
|
||||||
|
|
||||||
@Value("${somkit.logging.socket.log-file-path}")
|
|
||||||
public void setLog_File_Path(String log_File_Path) {
|
|
||||||
if(StringUtils.hasText(log_File_Path)){
|
|
||||||
//如果使用相对路径,转换为绝对路径
|
|
||||||
if(log_File_Path.startsWith("./")){
|
|
||||||
log_File_Path = PathUtils.resolve(log_File_Path);
|
|
||||||
}
|
|
||||||
Log_File_Path = log_File_Path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Value("${somkit.logging.socket.max-read-length}")
|
|
||||||
public void setMax_Read_Length(String max_Read_Length) {
|
|
||||||
if(StringUtils.hasText(max_Read_Length)){
|
|
||||||
Max_Read_Length = max_Read_Length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Value("${somkit.logging.socket.read-interval}")
|
|
||||||
public void setRead_Interval(String read_Interval) {
|
|
||||||
if(StringUtils.hasText(read_Interval)){
|
|
||||||
Read_Interval = read_Interval;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package cn.somkit.fmt.config;
|
|
||||||
|
|
||||||
import cn.somkit.fmt.utils.PathUtils;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class RocksDBConfig {
|
|
||||||
|
|
||||||
public static String RocksDB_Path = "./cache/rocksdb";
|
|
||||||
|
|
||||||
public static String RocksDB_Column_Family = "default";
|
|
||||||
|
|
||||||
@Value("${somkit.cache.rocks-db.path}")
|
|
||||||
public void setRocksDB_Path(String rocksDB_Path) {
|
|
||||||
if(StringUtils.hasText(rocksDB_Path)){
|
|
||||||
//如果使用相对路径,转换为绝对路径
|
|
||||||
if(rocksDB_Path.startsWith("./")){
|
|
||||||
rocksDB_Path = PathUtils.resolve(rocksDB_Path);
|
|
||||||
}
|
|
||||||
RocksDB_Path = rocksDB_Path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Value("${somkit.cache.rocks-db.column-family}")
|
|
||||||
public void setRocksDB_Column_Family(String rocksDB_Column_Family){
|
|
||||||
if(StringUtils.hasText(rocksDB_Column_Family)){
|
|
||||||
RocksDB_Column_Family = rocksDB_Column_Family;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
49
src/main/java/cn/somkit/fmt/entity/LoggerMessage.java
Normal file
49
src/main/java/cn/somkit/fmt/entity/LoggerMessage.java
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package cn.somkit.fmt.entity;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class LoggerMessage {
|
||||||
|
String level;
|
||||||
|
String loggerName;
|
||||||
|
String message;
|
||||||
|
String timestamp;
|
||||||
|
|
||||||
|
public LoggerMessage(String level, String loggerName, String message, String timestamp) {
|
||||||
|
this.level = level;
|
||||||
|
this.loggerName = loggerName;
|
||||||
|
this.message = message;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLevel() {
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLevel(String level) {
|
||||||
|
this.level = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLoggerName() {
|
||||||
|
return loggerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLoggerName(String loggerName) {
|
||||||
|
this.loggerName = loggerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimestamp(String timestamp) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/main/java/cn/somkit/fmt/filter/LogStashFilter.java
Normal file
44
src/main/java/cn/somkit/fmt/filter/LogStashFilter.java
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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.somkit.fmt.entity.LoggerMessage;
|
||||||
|
import cn.somkit.fmt.utils.LoggerQueue;
|
||||||
|
|
||||||
|
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")
|
||||||
|
);
|
||||||
|
LoggerQueue.getInstance().push(msg); // 单例阻塞队列
|
||||||
|
return FilterReply.NEUTRAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLevel(String level) {
|
||||||
|
this.level = Level.toLevel(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
if (this.level != null) {
|
||||||
|
super.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,6 @@ public class WebSocketAutoConfig implements WebSocketConfigurer {
|
|||||||
@Override
|
@Override
|
||||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||||
registry.addHandler(webSocketServerHandler(), "/socket/ws")//设置连接路径和处理
|
registry.addHandler(webSocketServerHandler(), "/socket/ws")//设置连接路径和处理
|
||||||
.setAllowedOrigins("*")//允许跨域访问
|
.setAllowedOrigins("*");//允许跨域访问
|
||||||
.addInterceptors(new WebSocketServerInterceptor());//设置拦截器
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package cn.somkit.fmt.socket;
|
|
||||||
|
|
||||||
public class WebSocketEntity {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送类型 HeartBeat:心跳 Message:消息
|
|
||||||
*/
|
|
||||||
private String send;
|
|
||||||
|
|
||||||
private Object data;
|
|
||||||
|
|
||||||
public String getSend() {
|
|
||||||
return send;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSend(String send) {
|
|
||||||
this.send = send;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Object getData() {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setData(Object data) {
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package cn.somkit.fmt.socket;
|
|
||||||
|
|
||||||
public enum WebSocketEnums {
|
|
||||||
|
|
||||||
//消息来源
|
|
||||||
SEND_TP_HEARTBEAT("HeartBeat", "心跳"),
|
|
||||||
SEND_TP_LOGGING("Logging", "日志")
|
|
||||||
;
|
|
||||||
|
|
||||||
private String code;
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
WebSocketEnums(){};
|
|
||||||
|
|
||||||
WebSocketEnums(String code, String name) {
|
|
||||||
this.code = code;
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCode() {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package cn.somkit.fmt.socket;
|
|
||||||
|
|
||||||
import org.springframework.web.socket.TextMessage;
|
|
||||||
import org.springframework.web.socket.WebSocketSession;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
|
|
||||||
public class WebSocketOnline {
|
|
||||||
|
|
||||||
//在线连接数
|
|
||||||
private static final AtomicInteger onlineCount = new AtomicInteger(0);
|
|
||||||
//在线连接
|
|
||||||
private static final Map<String, WebSocketSession> onlineMap = new ConcurrentHashMap<>();
|
|
||||||
//日志读取位置记录
|
|
||||||
public static final Map<String, Integer> logLengthMap = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
public static synchronized int getOnlineCount() {
|
|
||||||
return onlineCount.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setOnline(String id, WebSocketSession session) throws Exception{
|
|
||||||
//如果已经存在,先把原来的连接关闭
|
|
||||||
if(onlineMap.containsKey(id)){
|
|
||||||
if(onlineMap.get(id).isOpen()){
|
|
||||||
onlineMap.get(id).close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onlineMap.put(id, session);
|
|
||||||
onlineCount.set(getOnlineCount() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static WebSocketSession getOnline(String id) throws Exception{
|
|
||||||
return onlineMap.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<WebSocketSession> getOnlineList() throws Exception{
|
|
||||||
List<WebSocketSession> list = new ArrayList<>();
|
|
||||||
for (String key : onlineMap.keySet()) {
|
|
||||||
list.add(onlineMap.get(key));
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void closeOnline(String id) throws Exception{
|
|
||||||
onlineMap.remove(id);
|
|
||||||
onlineCount.set(getOnlineCount() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void sendMessage(String id, String entityJsonStr) throws Exception {
|
|
||||||
WebSocketSession session = getOnline(id);
|
|
||||||
if(session != null && session.isOpen()){
|
|
||||||
session.sendMessage(new TextMessage(entityJsonStr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +1,53 @@
|
|||||||
package cn.somkit.fmt.socket;
|
package cn.somkit.fmt.socket;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
import cn.somkit.fmt.utils.ErrorUtil;
|
import cn.metona.cache.Cache;
|
||||||
import cn.somkit.fmt.utils.LogSocketUtils;
|
import cn.somkit.fmt.entity.LoggerMessage;
|
||||||
|
import cn.somkit.fmt.utils.LoggerQueue;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.web.socket.CloseStatus;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.socket.WebSocketHandler;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.socket.WebSocketMessage;
|
import org.springframework.web.socket.*;
|
||||||
import org.springframework.web.socket.WebSocketSession;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Component
|
||||||
public class WebSocketServerHandler implements WebSocketHandler {
|
public class WebSocketServerHandler implements WebSocketHandler {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Cache<String, Object> cache;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||||
String id = session.getAttributes().get("id").toString();
|
push(session);
|
||||||
WebSocketOnline.setOnline(id, session);
|
}
|
||||||
logger.info("有新窗口开始监听:" + id + ", 当前在线人数为:" + WebSocketOnline.getOnlineCount());
|
|
||||||
try {
|
|
||||||
//返回一个心跳检测
|
|
||||||
WebSocketEntity entity = new WebSocketEntity();
|
|
||||||
entity.setSend(WebSocketEnums.SEND_TP_HEARTBEAT.getCode());
|
|
||||||
entity.setData("心跳检测");
|
|
||||||
WebSocketOnline.sendMessage(id, JSONUtil.toJsonStr(entity));
|
|
||||||
}catch (Exception e){
|
|
||||||
logger.error("连接成功发送心跳异常:{}", ErrorUtil.errorInfoToString(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
//读取并实时发送日志
|
private void push(WebSocketSession session) throws IOException {
|
||||||
LogSocketUtils.LoggingChannel(id);
|
while (StrUtil.isBlankIfStr(cache.get("closed")) || !Boolean.parseBoolean(String.valueOf(cache.get("closed")))) {
|
||||||
|
LoggerMessage log = LoggerQueue.getInstance().poll();
|
||||||
|
if(log != null){
|
||||||
|
session.sendMessage(new TextMessage(JSONUtil.toJsonStr(log)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
|
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
|
||||||
String id = session.getAttributes().get("id").toString();
|
push(session);
|
||||||
String messageStr = message.getPayload().toString();
|
|
||||||
WebSocketEntity entity = JSONUtil.toBean(messageStr, WebSocketEntity.class);
|
|
||||||
if(Objects.equals(WebSocketEnums.SEND_TP_HEARTBEAT.getCode(), entity.getSend())) {
|
|
||||||
//心跳检测消息,原路返回客户端
|
|
||||||
WebSocketOnline.sendMessage(id, JSONUtil.toJsonStr(entity));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleTransportError(WebSocketSession session, Throwable e) throws Exception {
|
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
|
||||||
String id = session.getAttributes().get("id").toString();
|
|
||||||
//发生错误时主动关闭连接
|
|
||||||
if(session.isOpen()){
|
|
||||||
session.close();
|
|
||||||
}
|
|
||||||
WebSocketOnline.closeOnline(id);
|
|
||||||
logger.error("{}连接发生错误:{}", id, ErrorUtil.errorInfoToString(e));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
|
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
|
||||||
String id = session.getAttributes().get("id").toString();
|
|
||||||
WebSocketOnline.closeOnline(id);
|
|
||||||
logger.info("连接关闭:{},状态:{},当前在线人数为:{}", id, closeStatus.toString(), WebSocketOnline.getOnlineCount());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
package cn.somkit.fmt.socket;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.springframework.http.server.ServerHttpRequest;
|
|
||||||
import org.springframework.http.server.ServerHttpResponse;
|
|
||||||
import org.springframework.http.server.ServletServerHttpRequest;
|
|
||||||
import org.springframework.http.server.ServletServerHttpResponse;
|
|
||||||
import org.springframework.web.socket.WebSocketHandler;
|
|
||||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class WebSocketServerInterceptor implements HandshakeInterceptor {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
|
|
||||||
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
|
|
||||||
HttpServletRequest rs = ((ServletServerHttpRequest) request).getServletRequest();
|
|
||||||
HttpServletResponse hp = ((ServletServerHttpResponse)response).getServletResponse();
|
|
||||||
//request 参数放入 attributes中
|
|
||||||
rs.getParameterMap().forEach((k, v) -> {
|
|
||||||
attributes.put(k, v[0]);
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
|
|
||||||
WebSocketHandler wsHandler, Exception exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
29
src/main/java/cn/somkit/fmt/utils/AppContext.java
Normal file
29
src/main/java/cn/somkit/fmt/utils/AppContext.java
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package cn.somkit.fmt.utils;
|
||||||
|
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class AppContext implements ApplicationContextAware {
|
||||||
|
|
||||||
|
private static ApplicationContext context;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||||
|
context = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ApplicationContext getApplicationContext() {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T getBean(Class<T> clazz) {
|
||||||
|
return context.getBean(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Object getBean(String name) {
|
||||||
|
return context.getBean(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package cn.somkit.fmt.utils;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.io.StringWriter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 捕获报错日志处理工具类
|
|
||||||
*/
|
|
||||||
public class ErrorUtil {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception出错的栈信息转成字符串
|
|
||||||
* 用于打印到日志中
|
|
||||||
*/
|
|
||||||
public static String errorInfoToString(Throwable e) {
|
|
||||||
StringWriter sw = null;
|
|
||||||
PrintWriter pw = null;
|
|
||||||
try {
|
|
||||||
sw = new StringWriter();
|
|
||||||
pw = new PrintWriter(sw);
|
|
||||||
// 将出错的栈信息输出到printWriter中
|
|
||||||
e.printStackTrace(pw);
|
|
||||||
pw.flush();
|
|
||||||
sw.flush();
|
|
||||||
} finally {
|
|
||||||
if (sw != null) {
|
|
||||||
try {
|
|
||||||
sw.close();
|
|
||||||
} catch (IOException e1) {
|
|
||||||
e1.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pw != null) {
|
|
||||||
pw.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sw.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
package cn.somkit.fmt.utils;
|
|
||||||
|
|
||||||
import cn.hutool.json.JSONUtil;
|
|
||||||
import cn.somkit.fmt.config.LogSocketConfig;
|
|
||||||
import cn.somkit.fmt.socket.WebSocketEntity;
|
|
||||||
import cn.somkit.fmt.socket.WebSocketEnums;
|
|
||||||
import cn.somkit.fmt.socket.WebSocketOnline;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.thymeleaf.util.StringUtils;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.FileReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public class LogSocketUtils {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(LogSocketUtils.class);
|
|
||||||
|
|
||||||
public static void LoggingChannel(String socketSessionId) {
|
|
||||||
//获取日志信息
|
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
boolean first = true;
|
|
||||||
while (WebSocketOnline.getOnline(socketSessionId) != null) {
|
|
||||||
BufferedReader reader = null;
|
|
||||||
try {
|
|
||||||
//日志文件路径
|
|
||||||
String filePath = ParamUtils.getLogFilePath();
|
|
||||||
//字符流
|
|
||||||
reader = new BufferedReader(new FileReader(filePath));
|
|
||||||
Object[] lines = reader.lines().toArray();
|
|
||||||
//只取从上次之后产生的日志
|
|
||||||
Integer length = WebSocketOnline.logLengthMap.get(socketSessionId);
|
|
||||||
Object[] copyOfRange = Arrays.copyOfRange(lines, length == null ? 0 : length, lines.length);
|
|
||||||
//对日志进行着色,更加美观 PS:注意,这里要根据日志生成规则来操作
|
|
||||||
for (int i = 0; i < copyOfRange.length; i++) {
|
|
||||||
String line = (String) copyOfRange[i];
|
|
||||||
|
|
||||||
//先转义
|
|
||||||
line = line.replaceAll("&", "&")
|
|
||||||
.replaceAll("<", "<")
|
|
||||||
.replaceAll(">", ">")
|
|
||||||
.replaceAll("\"", """);
|
|
||||||
|
|
||||||
String[] stars = line.split("->");
|
|
||||||
if(stars.length == 2){
|
|
||||||
line = line.replace(stars[0], "<span style='color: #298a8a;'>"+stars[0]+"</span>");
|
|
||||||
}
|
|
||||||
|
|
||||||
//处理等级
|
|
||||||
line = line.replace("[DEBUG ]", "[<span style='color: blue;'>DEBUG</span>]");
|
|
||||||
line = line.replace("[INFO ]", "[<span style='color: green;'>INFO</span>]");
|
|
||||||
line = line.replace("[WARN ]", "[<span style='color: orange;'>WARN</span>]");
|
|
||||||
line = line.replace("[ERROR ]", "[<span style='color: red;'>ERROR</span>]");
|
|
||||||
|
|
||||||
copyOfRange[i] = line;
|
|
||||||
}
|
|
||||||
//存储最新一行开始
|
|
||||||
WebSocketOnline.logLengthMap.put(socketSessionId, lines.length);
|
|
||||||
|
|
||||||
//第一次如果太大,截取最新的200行就够了,避免传输的数据太大
|
|
||||||
int maxLength = Integer.parseInt(ParamUtils.getMaxReadLength());
|
|
||||||
if(first && copyOfRange.length > maxLength){
|
|
||||||
copyOfRange = Arrays.copyOfRange(copyOfRange, copyOfRange.length - maxLength, copyOfRange.length);
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String result = StringUtils.join(copyOfRange, "<br/>");
|
|
||||||
WebSocketEntity entity = new WebSocketEntity();
|
|
||||||
entity.setSend(WebSocketEnums.SEND_TP_LOGGING.getCode());
|
|
||||||
entity.setData(result);
|
|
||||||
WebSocketOnline.sendMessage(socketSessionId, JSONUtil.toJsonStr(entity));
|
|
||||||
|
|
||||||
//休眠一秒
|
|
||||||
Thread.sleep(Long.parseLong(ParamUtils.getReadInterval()));
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("读取日志异常:{}", ErrorUtil.errorInfoToString(e));
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
assert reader != null;
|
|
||||||
reader.close();
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
logger.error("字符流关闭异常:{}", ErrorUtil.errorInfoToString(ioe));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("读取日志异常:{}", ErrorUtil.errorInfoToString(e));
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
28
src/main/java/cn/somkit/fmt/utils/LoggerQueue.java
Normal file
28
src/main/java/cn/somkit/fmt/utils/LoggerQueue.java
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
package cn.somkit.fmt.utils;
|
|
||||||
|
|
||||||
import cn.somkit.fmt.config.RocksDBConfig;
|
|
||||||
import org.rocksdb.*;
|
|
||||||
import org.springframework.util.ObjectUtils;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ConcurrentMap;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
public class RocksDBUtils {
|
|
||||||
|
|
||||||
private static RocksDB rocksDB;
|
|
||||||
public static ConcurrentMap<String, ColumnFamilyHandle> columnFamilyHandleMap = new ConcurrentHashMap<>();
|
|
||||||
public static int GET_KEYS_BATCH_SIZE = 100000;
|
|
||||||
|
|
||||||
static {
|
|
||||||
try {
|
|
||||||
String rocksDBPath = RocksDBConfig.RocksDB_Path; //RocksDB文件目录
|
|
||||||
PathUtils.directory(rocksDBPath); // 创建RocksDB文件目录
|
|
||||||
RocksDB.loadLibrary();
|
|
||||||
Options options = new Options();
|
|
||||||
options.setCreateIfMissing(true); //如果数据库不存在则创建
|
|
||||||
List<byte[]> cfArr = RocksDB.listColumnFamilies(options, rocksDBPath); // 初始化所有已存在列族
|
|
||||||
List<ColumnFamilyDescriptor> columnFamilyDescriptors = new ArrayList<>(); //ColumnFamilyDescriptor集合
|
|
||||||
if (!ObjectUtils.isEmpty(cfArr)) {
|
|
||||||
for (byte[] cf : cfArr) {
|
|
||||||
columnFamilyDescriptors.add(new ColumnFamilyDescriptor(cf, new ColumnFamilyOptions()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
columnFamilyDescriptors.add(new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, new ColumnFamilyOptions()));
|
|
||||||
}
|
|
||||||
DBOptions dbOptions = new DBOptions();
|
|
||||||
dbOptions.setCreateIfMissing(true);
|
|
||||||
List<ColumnFamilyHandle> columnFamilyHandles = new ArrayList<>(); //ColumnFamilyHandle集合
|
|
||||||
rocksDB = RocksDB.open(dbOptions, rocksDBPath, columnFamilyDescriptors, columnFamilyHandles);
|
|
||||||
for (int i = 0; i < columnFamilyDescriptors.size(); i++) {
|
|
||||||
ColumnFamilyHandle columnFamilyHandle = columnFamilyHandles.get(i);
|
|
||||||
String cfName = new String(columnFamilyDescriptors.get(i).getName(), StandardCharsets.UTF_8);
|
|
||||||
columnFamilyHandleMap.put(cfName, columnFamilyHandle);
|
|
||||||
}
|
|
||||||
System.out.println("RocksDB init success!! path:" + rocksDBPath);
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println("RocksDB init failure!! error:" + e.getMessage());
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private RocksDBUtils(){}
|
|
||||||
|
|
||||||
public static ColumnFamilyHandle cfAddIfNotExist(String cfName) throws RocksDBException {
|
|
||||||
ColumnFamilyHandle columnFamilyHandle;
|
|
||||||
if (!columnFamilyHandleMap.containsKey(cfName)) {
|
|
||||||
columnFamilyHandle = rocksDB.createColumnFamily(new ColumnFamilyDescriptor(cfName.getBytes(), new ColumnFamilyOptions()));
|
|
||||||
columnFamilyHandleMap.put(cfName, columnFamilyHandle);
|
|
||||||
System.out.println("cfAddIfNotExist success!! cfName:" + cfName);
|
|
||||||
} else {
|
|
||||||
columnFamilyHandle = columnFamilyHandleMap.get(cfName);
|
|
||||||
}
|
|
||||||
return columnFamilyHandle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 列族,删除(如果存在)
|
|
||||||
*/
|
|
||||||
public static void cfDeleteIfExist(String cfName) throws RocksDBException {
|
|
||||||
if (columnFamilyHandleMap.containsKey(cfName)) {
|
|
||||||
rocksDB.dropColumnFamily(columnFamilyHandleMap.get(cfName));
|
|
||||||
columnFamilyHandleMap.remove(cfName);
|
|
||||||
System.out.println("cfDeleteIfExist success!! cfName:" + cfName);
|
|
||||||
} else {
|
|
||||||
System.out.println("cfDeleteIfExist containsKey!! cfName:" + cfName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 增
|
|
||||||
*/
|
|
||||||
public static void put(String cfName, String key, String value) throws RocksDBException {
|
|
||||||
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
|
|
||||||
rocksDB.put(columnFamilyHandle, key.getBytes(), value.getBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 增(批量)
|
|
||||||
*/
|
|
||||||
public static void batchPut(String cfName, Map<String, String> map) throws RocksDBException {
|
|
||||||
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
|
|
||||||
WriteOptions writeOptions = new WriteOptions();
|
|
||||||
WriteBatch writeBatch = new WriteBatch();
|
|
||||||
for (Map.Entry<String, String> entry : map.entrySet()) {
|
|
||||||
writeBatch.put(columnFamilyHandle, entry.getKey().getBytes(), entry.getValue().getBytes());
|
|
||||||
}
|
|
||||||
rocksDB.write(writeOptions, writeBatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删
|
|
||||||
*/
|
|
||||||
public static void delete(String cfName, String key) throws RocksDBException {
|
|
||||||
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
|
|
||||||
rocksDB.delete(columnFamilyHandle, key.getBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查
|
|
||||||
*/
|
|
||||||
public static String get(String cfName, String key) throws RocksDBException {
|
|
||||||
String value = null;
|
|
||||||
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
|
|
||||||
byte[] bytes = rocksDB.get(columnFamilyHandle, key.getBytes());
|
|
||||||
if (!ObjectUtils.isEmpty(bytes)) {
|
|
||||||
value = new String(bytes, StandardCharsets.UTF_8);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查(多个键值对)
|
|
||||||
*/
|
|
||||||
public static Map<String, String> multiGetAsMap(String cfName, List<String> keys) throws RocksDBException {
|
|
||||||
Map<String, String> map = new HashMap<>(keys.size());
|
|
||||||
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
|
|
||||||
List<ColumnFamilyHandle> columnFamilyHandles;
|
|
||||||
List<byte[]> keyBytes = keys.stream().map(String::getBytes).collect(Collectors.toList());
|
|
||||||
columnFamilyHandles = IntStream.range(0, keys.size()).mapToObj(i -> columnFamilyHandle).collect(Collectors.toList());
|
|
||||||
List<byte[]> bytes = rocksDB.multiGetAsList(columnFamilyHandles, keyBytes);
|
|
||||||
for (int i = 0; i < bytes.size(); i++) {
|
|
||||||
byte[] valueBytes = bytes.get(i);
|
|
||||||
String value = "";
|
|
||||||
if (!ObjectUtils.isEmpty(valueBytes)) {
|
|
||||||
value = new String(valueBytes, StandardCharsets.UTF_8);
|
|
||||||
}
|
|
||||||
map.put(keys.get(i), value);
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查(多个值)
|
|
||||||
*/
|
|
||||||
public static List<String> multiGetValueAsList(String cfName, List<String> keys) throws RocksDBException {
|
|
||||||
List<String> values = new ArrayList<>(keys.size());
|
|
||||||
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
|
|
||||||
List<ColumnFamilyHandle> columnFamilyHandles = new ArrayList<>();
|
|
||||||
List<byte[]> keyBytes = keys.stream().map(String::getBytes).collect(Collectors.toList());
|
|
||||||
for (int i = 0; i < keys.size(); i++) {
|
|
||||||
columnFamilyHandles.add(columnFamilyHandle);
|
|
||||||
}
|
|
||||||
List<byte[]> bytes = rocksDB.multiGetAsList(columnFamilyHandles, keyBytes);
|
|
||||||
for (byte[] valueBytes : bytes) {
|
|
||||||
String value = "";
|
|
||||||
if (!ObjectUtils.isEmpty(valueBytes)) {
|
|
||||||
value = new String(valueBytes, StandardCharsets.UTF_8);
|
|
||||||
}
|
|
||||||
values.add(value);
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查(所有键)
|
|
||||||
*/
|
|
||||||
public static List<String> getAllKey(String cfName) throws RocksDBException {
|
|
||||||
List<String> list = new ArrayList<>();
|
|
||||||
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
|
|
||||||
try (RocksIterator rocksIterator = rocksDB.newIterator(columnFamilyHandle)) {
|
|
||||||
for (rocksIterator.seekToFirst(); rocksIterator.isValid(); rocksIterator.next()) {
|
|
||||||
list.add(new String(rocksIterator.key(), StandardCharsets.UTF_8));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分片查(键)
|
|
||||||
*/
|
|
||||||
public static List<String> getKeysFrom(String cfName, String lastKey) throws RocksDBException {
|
|
||||||
List<String> list = new ArrayList<>(GET_KEYS_BATCH_SIZE);
|
|
||||||
// 获取列族Handle
|
|
||||||
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName);
|
|
||||||
try (RocksIterator rocksIterator = rocksDB.newIterator(columnFamilyHandle)) {
|
|
||||||
if (lastKey != null) {
|
|
||||||
rocksIterator.seek(lastKey.getBytes(StandardCharsets.UTF_8));
|
|
||||||
rocksIterator.next();
|
|
||||||
} else {
|
|
||||||
rocksIterator.seekToFirst();
|
|
||||||
}
|
|
||||||
// 一批次最多 GET_KEYS_BATCH_SIZE 个 key
|
|
||||||
while (rocksIterator.isValid() && list.size() < GET_KEYS_BATCH_SIZE) {
|
|
||||||
list.add(new String(rocksIterator.key(), StandardCharsets.UTF_8));
|
|
||||||
rocksIterator.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查(所有键值)
|
|
||||||
*/
|
|
||||||
public static Map<String, String> getAll(String cfName) throws RocksDBException {
|
|
||||||
Map<String, String> map = new HashMap<>();
|
|
||||||
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
|
|
||||||
try (RocksIterator rocksIterator = rocksDB.newIterator(columnFamilyHandle)) {
|
|
||||||
for (rocksIterator.seekToFirst(); rocksIterator.isValid(); rocksIterator.next()) {
|
|
||||||
map.put(new String(rocksIterator.key(), StandardCharsets.UTF_8), new String(rocksIterator.value(), StandardCharsets.UTF_8));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查总条数
|
|
||||||
*/
|
|
||||||
public static int getCount(String cfName) throws RocksDBException {
|
|
||||||
int count = 0;
|
|
||||||
ColumnFamilyHandle columnFamilyHandle = cfAddIfNotExist(cfName); //获取列族Handle
|
|
||||||
try (RocksIterator rocksIterator = rocksDB.newIterator(columnFamilyHandle)) {
|
|
||||||
for (rocksIterator.seekToFirst(); rocksIterator.isValid(); rocksIterator.next()) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,23 +15,52 @@ spring:
|
|||||||
multipart:
|
multipart:
|
||||||
max-file-size: 1024MB
|
max-file-size: 1024MB
|
||||||
max-request-size: 10240MB
|
max-request-size: 10240MB
|
||||||
|
threads:
|
||||||
|
virtual:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
somkit:
|
somkit:
|
||||||
upload:
|
upload:
|
||||||
path: ./data/files #上传文件路径 可以使用绝对路径,也可使用相对路径
|
path:
|
||||||
temp-path: ./data/files/temp #临时文件存放地址 可以使用绝对路径,也可使用相对路径
|
windows: D://data/install/upload
|
||||||
cache:
|
linux: /mnt/files
|
||||||
rocks-db:
|
|
||||||
path: ./cache/rocksdb # RocksDB缓存路径 可以使用绝对路径,也可使用相对路径
|
|
||||||
column-family: default
|
|
||||||
logging:
|
|
||||||
socket:
|
|
||||||
#日志文件地址
|
|
||||||
log-file-path: ./logs/fmt-server.log
|
|
||||||
#最大读取展示行数
|
|
||||||
max-read-length: 500
|
|
||||||
#读取间隔时间 毫秒
|
|
||||||
read-interval: 1000
|
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
config: classpath:logback-spring.xml
|
config: classpath:logback-spring.xml
|
||||||
|
|
||||||
|
metona:
|
||||||
|
cache:
|
||||||
|
# 缓存类型,支持以下类型:
|
||||||
|
# - CONCURRENT_HASH_MAP: 基于 ConcurrentHashMap 的线程安全缓存
|
||||||
|
# - WEAK_HASH_MAP: 基于 WeakHashMap 的弱引用缓存
|
||||||
|
# - LINKED_HASH_MAP: 基于 LinkedHashMap 的 LRU 缓存
|
||||||
|
type: LINKED_HASH_MAP
|
||||||
|
|
||||||
|
# 缓存的初始容量,默认值为 16
|
||||||
|
initial-capacity: 128
|
||||||
|
|
||||||
|
# 缓存的最大容量,当缓存条目数超过该值时,会根据策略移除旧条目
|
||||||
|
# 仅对 LINKED_HASH_MAP 类型有效
|
||||||
|
maximum-size: 2000
|
||||||
|
|
||||||
|
# 写入后过期时间(单位由 time-unit 指定)
|
||||||
|
# 默认值为 -1,表示永不过期
|
||||||
|
expire-after-write: -1
|
||||||
|
|
||||||
|
# 访问后过期时间(单位由 time-unit 指定)
|
||||||
|
# 默认值为 -1,表示永不过期
|
||||||
|
expire-after-access: -1
|
||||||
|
|
||||||
|
# 时间单位,支持以下值:
|
||||||
|
# - NANOSECONDS: 纳秒
|
||||||
|
# - MICROSECONDS: 微秒
|
||||||
|
# - MILLISECONDS: 毫秒(默认)
|
||||||
|
# - SECONDS: 秒
|
||||||
|
# - MINUTES: 分钟
|
||||||
|
# - HOURS: 小时
|
||||||
|
# - DAYS: 天
|
||||||
|
time-unit: MILLISECONDS
|
||||||
|
|
||||||
|
# 是否记录缓存统计信息(如命中率、加载次数等)
|
||||||
|
# 默认值为 false
|
||||||
|
record-stats: true
|
||||||
@@ -3,15 +3,10 @@
|
|||||||
<!-- Spring Boot 默认日志配置 -->
|
<!-- Spring Boot 默认日志配置 -->
|
||||||
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
|
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
|
||||||
|
|
||||||
<!-- 定义日志的根目录 -->
|
|
||||||
<property name="LOG_HOME" value="./logs"/>
|
|
||||||
<!-- 定义日志文件名称 -->
|
|
||||||
<property name="APP_NAME" value="fmt-server"/>
|
|
||||||
|
|
||||||
<!-- 日志输出到控制台 -->
|
<!-- 日志输出到控制台 -->
|
||||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
<filter class="cn.somkit.fmt.filter.LogStashFilter">
|
||||||
<level>INFO</level>
|
<level>DEBUG</level>
|
||||||
</filter>
|
</filter>
|
||||||
<encoder>
|
<encoder>
|
||||||
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||||
@@ -19,43 +14,8 @@
|
|||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
|
|
||||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
|
||||||
<!-- 指定日志文件的名称 -->
|
|
||||||
<file>${LOG_HOME}/${APP_NAME}.log</file>
|
|
||||||
<!--
|
|
||||||
当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名
|
|
||||||
TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。
|
|
||||||
-->
|
|
||||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
|
||||||
<!--
|
|
||||||
滚动时产生的文件的存放位置及文件名称 %d{yyyy-MM-dd}:按天进行日志滚动
|
|
||||||
%i:当文件大小超过maxFileSize时,按照i进行文件滚动
|
|
||||||
-->
|
|
||||||
<fileNamePattern>${LOG_HOME}/${APP_NAME}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
|
|
||||||
<!--
|
|
||||||
可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,
|
|
||||||
且maxHistory是365,则只保存最近365天的文件,删除之前的旧文件。注意,删除旧文件是,
|
|
||||||
那些为了归档而创建的目录也会被删除。
|
|
||||||
-->
|
|
||||||
<MaxHistory>365</MaxHistory>
|
|
||||||
<!--
|
|
||||||
当日志文件超过maxFileSize指定的大小是,根据上面提到的%i进行日志文件滚动 注意此处配置SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的,必须配置timeBasedFileNamingAndTriggeringPolicy
|
|
||||||
-->
|
|
||||||
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
|
||||||
<maxFileSize>100MB</maxFileSize>
|
|
||||||
</timeBasedFileNamingAndTriggeringPolicy>
|
|
||||||
</rollingPolicy>
|
|
||||||
<!-- 日志输出格式: -->
|
|
||||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
|
||||||
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%-5level] [%logger{50}] -> %msg%n</pattern>
|
|
||||||
<charset>UTF-8</charset>
|
|
||||||
</layout>
|
|
||||||
</appender>
|
|
||||||
|
|
||||||
<!-- 指定日志输出级别,以及启动的Appender -->
|
<!-- 指定日志输出级别,以及启动的Appender -->
|
||||||
<root level="INFO">
|
<root level="DEBUG">
|
||||||
<appender-ref ref="CONSOLE"/>
|
<appender-ref ref="CONSOLE"/>
|
||||||
<appender-ref ref="FILE"/>
|
|
||||||
</root>
|
</root>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
459
src/main/resources/static/common/js/LogMonitorAdaptive.js
Normal file
459
src/main/resources/static/common/js/LogMonitorAdaptive.js
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
class LogMonitorAdaptive {
|
||||||
|
constructor(container, opts = {}) {
|
||||||
|
this.container = typeof container === 'string'
|
||||||
|
? document.querySelector(container)
|
||||||
|
: container;
|
||||||
|
if (!this.container) throw new Error('容器未找到');
|
||||||
|
|
||||||
|
/* ========== 配置项及其含义 ========== */
|
||||||
|
this.cfg = {
|
||||||
|
/* --- 尺寸相关 --- */
|
||||||
|
width: null, // 日志容器宽度。null=100%,可填数字(px)或字符串('80%'/'20rem')
|
||||||
|
height: null, // 日志容器高度。null=100vh,可填数字(px)或字符串('400px'/'50vh')
|
||||||
|
|
||||||
|
/* --- 日志条数与滚动 --- */
|
||||||
|
maxLines: 5000, // 最大保留日志条数,超出后自动删除最旧日志
|
||||||
|
autoScroll: true, // 出现新日志时是否自动滚动到底部
|
||||||
|
|
||||||
|
/* --- 主题与外观 --- */
|
||||||
|
theme: 'dark', // 主题:'dark' | 'light',控制整体配色
|
||||||
|
fontSize: 14, // 日志文字大小,单位 px
|
||||||
|
wordWrap: false, // 日志内容是否自动换行(true=换行,false=横向滚动)
|
||||||
|
|
||||||
|
/* --- 日志内容格式 --- */
|
||||||
|
showTimestamp: true, // 是否显示时间戳
|
||||||
|
showLevel: true, // 是否显示日志级别标签
|
||||||
|
timeFormat: 'HH:mm:ss.SSS', // 时间戳格式,可自定义
|
||||||
|
|
||||||
|
/* --- 日志级别 & 过滤 --- */
|
||||||
|
levels: ['debug', 'info', 'warn', 'error', 'success', 'system'], // 可用日志级别
|
||||||
|
// 过滤功能基于 levels;如只想显示 info/warn,可在这里删减
|
||||||
|
|
||||||
|
/* --- 功能开关 --- */
|
||||||
|
enableFilter: true, // 顶部是否显示级别过滤复选框
|
||||||
|
enableSearch: true, // 是否启用搜索框与高亮功能
|
||||||
|
enableExport: true, // 是否允许导出日志文件
|
||||||
|
enableClear: true, // 是否提供“清空”按钮
|
||||||
|
enablePause: true, // 是否提供“暂停/继续”按钮
|
||||||
|
enableThemeToggle: true, // 是否提供“切换主题”按钮
|
||||||
|
enableFullscreen: true, // 是否提供“全屏”按钮
|
||||||
|
enableFontSize: true, // 是否提供“字体大小 +/-”按钮
|
||||||
|
enableWordWrap: true, // 是否提供“换行/不换行”切换按钮
|
||||||
|
|
||||||
|
/* --- 暂停/继续 的回调函数 --- */
|
||||||
|
onTogglePause: () => {},
|
||||||
|
|
||||||
|
/* --- 创建完成 的回调函数 --- */
|
||||||
|
onCreated: () => {},
|
||||||
|
|
||||||
|
...opts
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 强制确保 levels 是数组,防止 forEach 报错 */
|
||||||
|
if (!Array.isArray(this.cfg.levels)) {
|
||||||
|
this.cfg.levels = ['info', 'warn', 'error'];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isPaused = false;
|
||||||
|
this.isFullscreen = false;
|
||||||
|
this.logs = [];
|
||||||
|
this.filters = new Set();
|
||||||
|
this.searchTerm = '';
|
||||||
|
this.highlightIndex = -1;
|
||||||
|
this.highlightEls = [];
|
||||||
|
|
||||||
|
this.initDOM();
|
||||||
|
this.bindResize();
|
||||||
|
this.bindGlobalEvents();
|
||||||
|
|
||||||
|
//执行回调函数
|
||||||
|
if(this.cfg.onCreated && typeof this.cfg.onCreated === 'function'){
|
||||||
|
this.cfg.onCreated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------ 初始化 ------------------------ */
|
||||||
|
initDOM() {
|
||||||
|
this.container.innerHTML = '';
|
||||||
|
this.setContainerSize(); // 宽高自适应
|
||||||
|
this.applyContainerStyle();
|
||||||
|
|
||||||
|
// 顶部导航栏
|
||||||
|
this.navbar = document.createElement('div');
|
||||||
|
Object.assign(this.navbar.style, {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '8px 12px',
|
||||||
|
backgroundColor: this.cfg.theme === 'dark' ? '#2d2d30' : '#f3f3f3',
|
||||||
|
borderBottom: `1px solid ${this.cfg.theme === 'dark' ? '#555' : '#ccc'}`,
|
||||||
|
gap: '10px',
|
||||||
|
fontSize: '14px',
|
||||||
|
flexWrap: 'wrap'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按钮组
|
||||||
|
if (this.cfg.enablePause) {
|
||||||
|
this.pauseBtn = this.createBtn('⏸ 暂停', () => this.togglePause(), {w: '72px'});
|
||||||
|
this.navbar.appendChild(this.pauseBtn);
|
||||||
|
}
|
||||||
|
if (this.cfg.enableClear) {
|
||||||
|
this.navbar.appendChild(this.createBtn('🗑 清空', () => this.clear(), {w: '72px'}));
|
||||||
|
}
|
||||||
|
if (this.cfg.enableExport) {
|
||||||
|
this.navbar.appendChild(this.createBtn('💾 导出', () => this.export(), {w: '72px'}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主题切换按钮
|
||||||
|
if (this.cfg.enableThemeToggle) {
|
||||||
|
this.themeBtn = this.createBtn(
|
||||||
|
this.cfg.theme === 'dark' ? '☀️ 明亮' : '🌙 暗黑',
|
||||||
|
() => this.toggleTheme(),
|
||||||
|
{w: '80px'}
|
||||||
|
);
|
||||||
|
this.navbar.appendChild(this.themeBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字体大小
|
||||||
|
if (this.cfg.enableFontSize) {
|
||||||
|
this.navbar.appendChild(this.createBtn('A-', () => this.changeFontSize(-1), {w: '36px'}));
|
||||||
|
this.navbar.appendChild(this.createBtn('A+', () => this.changeFontSize(1), {w: '36px'}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 换行
|
||||||
|
if (this.cfg.enableWordWrap) {
|
||||||
|
this.wrapBtn = this.createBtn(
|
||||||
|
this.cfg.wordWrap ? '⏎ 换行' : '⏎ 不换行',
|
||||||
|
() => this.toggleWordWrap(),
|
||||||
|
{w: '90px'}
|
||||||
|
);
|
||||||
|
this.navbar.appendChild(this.wrapBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全屏
|
||||||
|
if (this.cfg.enableFullscreen) {
|
||||||
|
this.navbar.appendChild(this.createBtn('⛶ 全屏', () => this.toggleFullscreen(), {w: '72px'}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤复选框
|
||||||
|
if (this.cfg.enableFilter) {
|
||||||
|
this.cfg.levels.forEach(lvl => {
|
||||||
|
this.navbar.appendChild(this.createCheckbox(lvl));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索框
|
||||||
|
if (this.cfg.enableSearch) {
|
||||||
|
const searchWrap = document.createElement('span');
|
||||||
|
searchWrap.style.marginLeft = 'auto';
|
||||||
|
searchWrap.style.display = 'flex';
|
||||||
|
searchWrap.style.alignItems = 'center';
|
||||||
|
searchWrap.style.gap = '4px';
|
||||||
|
|
||||||
|
this.searchInput = document.createElement('input');
|
||||||
|
Object.assign(this.searchInput.style, {
|
||||||
|
padding: '6px 8px',
|
||||||
|
fontSize: '14px',
|
||||||
|
border: `1px solid ${this.cfg.theme === 'dark' ? '#555' : '#bbb'}`,
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor: this.cfg.theme === 'dark' ? '#3c3c3c' : '#fff',
|
||||||
|
color: 'inherit',
|
||||||
|
width: '260px'
|
||||||
|
});
|
||||||
|
this.searchInput.placeholder = '搜索…';
|
||||||
|
this.searchInput.oninput = () => {
|
||||||
|
this.searchTerm = this.searchInput.value.trim();
|
||||||
|
this.renderVisible();
|
||||||
|
};
|
||||||
|
searchWrap.appendChild(this.searchInput);
|
||||||
|
|
||||||
|
['↑', '↓'].forEach((arr, idx) => {
|
||||||
|
searchWrap.appendChild(
|
||||||
|
this.createBtn(arr, () => this.navigateHighlight(idx), {w: '30px'})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.navbar.appendChild(searchWrap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志视窗
|
||||||
|
this.viewport = document.createElement('div');
|
||||||
|
this.applyViewportStyle();
|
||||||
|
this.container.appendChild(this.navbar);
|
||||||
|
this.container.appendChild(this.viewport);
|
||||||
|
|
||||||
|
this.renderVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------ 样式 ------------------------ */
|
||||||
|
setContainerSize() {
|
||||||
|
// 如果用户未指定宽高,则自适应
|
||||||
|
if (this.cfg.width === null) {
|
||||||
|
this.container.style.width = '100%';
|
||||||
|
} else {
|
||||||
|
this.container.style.width = typeof this.cfg.width === 'number' ? `${this.cfg.width}px` : this.cfg.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cfg.height === null) {
|
||||||
|
//this.container.style.height = '100vh';
|
||||||
|
const bodyMargin = parseFloat(getComputedStyle(document.body).marginTop || 0) +
|
||||||
|
parseFloat(getComputedStyle(document.body).marginBottom || 0);
|
||||||
|
this.container.style.height = `calc(100vh - ${bodyMargin}px - 2px)`;
|
||||||
|
} else {
|
||||||
|
this.container.style.height = typeof this.cfg.height === 'number' ? `${this.cfg.height}px` : this.cfg.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyContainerStyle() {
|
||||||
|
Object.assign(this.container.style, {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
border: `1px solid ${this.cfg.theme === 'dark' ? '#444' : '#ccc'}`,
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontFamily: 'Consolas, "Courier New", monospace',
|
||||||
|
fontSize: `${this.cfg.fontSize}px`,
|
||||||
|
lineHeight: 1.45,
|
||||||
|
backgroundColor: this.cfg.theme === 'dark' ? '#1e1e1e' : '#fafafa',
|
||||||
|
color: this.cfg.theme === 'dark' ? '#d4d4d4' : '#222',
|
||||||
|
overflow: 'hidden'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
applyViewportStyle() {
|
||||||
|
Object.assign(this.viewport.style, {
|
||||||
|
flex: 1,
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: '8px',
|
||||||
|
whiteSpace: this.cfg.wordWrap ? 'pre-wrap' : 'pre',
|
||||||
|
wordBreak: 'break-word'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------ 工具 ------------------------ */
|
||||||
|
createBtn(text, handler, {w} = {}) {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.textContent = text;
|
||||||
|
Object.assign(btn.style, {
|
||||||
|
padding: '6px 10px',
|
||||||
|
fontSize: '14px',
|
||||||
|
backgroundColor: this.cfg.theme === 'dark' ? '#444' : '#e7e7e7',
|
||||||
|
color: 'inherit',
|
||||||
|
border: `1px solid ${this.cfg.theme === 'dark' ? '#666' : '#ccc'}`,
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
minWidth: w || 'auto'
|
||||||
|
});
|
||||||
|
btn.onmouseenter = () => btn.style.backgroundColor = this.cfg.theme === 'dark' ? '#555' : '#d0d0d0';
|
||||||
|
btn.onmouseleave = () => btn.style.backgroundColor = this.cfg.theme === 'dark' ? '#444' : '#e7e7e7';
|
||||||
|
btn.onclick = handler;
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
createCheckbox(level) {
|
||||||
|
const lbl = document.createElement('label');
|
||||||
|
lbl.style.color = this.cfg.theme === 'dark' ? '#ccc' : '#333';
|
||||||
|
lbl.style.fontSize = '13px';
|
||||||
|
lbl.style.display = 'flex';
|
||||||
|
lbl.style.alignItems = 'center';
|
||||||
|
|
||||||
|
const chk = document.createElement('input');
|
||||||
|
chk.type = 'checkbox';
|
||||||
|
chk.checked = true;
|
||||||
|
chk.style.margin = '0 4px';
|
||||||
|
chk.onchange = () => {
|
||||||
|
if (chk.checked) this.filters.delete(level);
|
||||||
|
else this.filters.add(level);
|
||||||
|
this.renderVisible();
|
||||||
|
};
|
||||||
|
lbl.appendChild(chk);
|
||||||
|
lbl.append(level.toUpperCase());
|
||||||
|
return lbl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------ 日志渲染 ------------------------ */
|
||||||
|
log(message, level = 'info', ts = this.formatTime(new Date())) {
|
||||||
|
if (!this.cfg.levels.includes(level)) level = 'info';
|
||||||
|
if (this.isPaused) return;
|
||||||
|
|
||||||
|
ts = this.cfg.showTimestamp ? ts : '';
|
||||||
|
this.logs.push({message, level, ts, id: Date.now() + Math.random()});
|
||||||
|
|
||||||
|
if (this.logs.length > this.cfg.maxLines) this.logs.shift();
|
||||||
|
this.renderVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderVisible() {
|
||||||
|
this.viewport.innerHTML = '';
|
||||||
|
this.highlightEls = [];
|
||||||
|
this.highlightIndex = -1;
|
||||||
|
|
||||||
|
const filtered = this.logs.filter(l => !this.filters.has(l.level));
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
const regex = this.searchTerm ? new RegExp(`(${this.searchTerm})`, 'gi') : null;
|
||||||
|
|
||||||
|
filtered.forEach(entry => {
|
||||||
|
const line = document.createElement('div');
|
||||||
|
line.style.display = 'flex';
|
||||||
|
line.style.margin = '1px 0';
|
||||||
|
|
||||||
|
if (this.cfg.showTimestamp && entry.ts) {
|
||||||
|
const tsSpan = document.createElement('span');
|
||||||
|
tsSpan.textContent = entry.ts + ' ';
|
||||||
|
tsSpan.style.color = this.cfg.theme === 'dark' ? '#6a9955' : '#008000';
|
||||||
|
tsSpan.style.minWidth = '90px';
|
||||||
|
line.appendChild(tsSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cfg.showLevel) {
|
||||||
|
const lvlSpan = document.createElement('span');
|
||||||
|
lvlSpan.textContent = `[${entry.level.toUpperCase()}]`;
|
||||||
|
const colors = {
|
||||||
|
debug: '#9c27b0', info: '#2196f3', warn: '#ff9800',
|
||||||
|
error: '#f44336', success: '#4caf50', system: '#00bcd4'
|
||||||
|
};
|
||||||
|
lvlSpan.style.color = colors[entry.level] || colors.info;
|
||||||
|
lvlSpan.style.minWidth = '70px';
|
||||||
|
lvlSpan.style.fontWeight = 'bold';
|
||||||
|
line.appendChild(lvlSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgSpan = document.createElement('span');
|
||||||
|
msgSpan.style.flex = 1;
|
||||||
|
let msg = entry.message;
|
||||||
|
if (regex) {
|
||||||
|
if (regex.test(entry.message)) {
|
||||||
|
line.style.backgroundColor = 'rgba(255,235,59,0.25)';
|
||||||
|
this.highlightEls.push(line);
|
||||||
|
}
|
||||||
|
msg = entry.message.replace(regex, '<mark style="background:#ffeb3b;color:#000;">$1</mark>');
|
||||||
|
}
|
||||||
|
msgSpan.innerHTML = msg;
|
||||||
|
line.appendChild(msgSpan);
|
||||||
|
frag.appendChild(line);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.viewport.appendChild(frag);
|
||||||
|
if (this.cfg.autoScroll && !this.isPaused) {
|
||||||
|
this.viewport.scrollTop = this.viewport.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------ 交互 ------------------------ */
|
||||||
|
togglePause() {
|
||||||
|
this.isPaused = !this.isPaused;
|
||||||
|
this.pauseBtn.textContent = this.isPaused ? '▶ 继续' : '⏸ 暂停';
|
||||||
|
this.pauseBtn.style.backgroundColor = this.isPaused
|
||||||
|
? (this.cfg.theme === 'dark' ? '#d32f2f' : '#ff5252')
|
||||||
|
: (this.cfg.theme === 'dark' ? '#444' : '#e7e7e7');
|
||||||
|
//执行回调函数
|
||||||
|
if(this.cfg.onTogglePause && typeof this.cfg.onTogglePause === 'function'){
|
||||||
|
this.cfg.onTogglePause(this.isPaused);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTheme() {
|
||||||
|
this.cfg.theme = this.cfg.theme === 'dark' ? 'light' : 'dark';
|
||||||
|
this.themeBtn.textContent = this.cfg.theme === 'dark' ? '☀️ 明亮' : '🌙 暗黑';
|
||||||
|
this.applyContainerStyle();
|
||||||
|
this.navbar.style.backgroundColor = this.cfg.theme === 'dark' ? '#2d2d30' : '#f3f3f3';
|
||||||
|
this.renderVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
changeFontSize(delta) {
|
||||||
|
this.cfg.fontSize = Math.max(10, Math.min(24, this.cfg.fontSize + delta));
|
||||||
|
this.container.style.fontSize = `${this.cfg.fontSize}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleWordWrap() {
|
||||||
|
this.cfg.wordWrap = !this.cfg.wordWrap;
|
||||||
|
this.wrapBtn.textContent = this.cfg.wordWrap ? '⏎ 不换行' : '⏎ 换行';
|
||||||
|
this.applyViewportStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleFullscreen() {
|
||||||
|
if (!this.isFullscreen) {
|
||||||
|
if (this.container.requestFullscreen) this.container.requestFullscreen();
|
||||||
|
else if (this.container.webkitRequestFullscreen) this.container.webkitRequestFullscreen();
|
||||||
|
else if (this.container.msRequestFullscreen) this.container.msRequestFullscreen();
|
||||||
|
} else {
|
||||||
|
if (document.exitFullscreen) document.exitFullscreen();
|
||||||
|
else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
|
||||||
|
else if (document.msExitFullscreen) document.msExitFullscreen();
|
||||||
|
}
|
||||||
|
this.isFullscreen = !this.isFullscreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateHighlight(dir) {
|
||||||
|
if (!this.highlightEls.length) return;
|
||||||
|
this.highlightIndex = (this.highlightIndex + (dir === 0 ? -1 : 1) + this.highlightEls.length) % this.highlightEls.length;
|
||||||
|
this.highlightEls.forEach((el, idx) => {
|
||||||
|
el.style.outline = idx === this.highlightIndex ? '2px solid #ffeb3b' : 'none';
|
||||||
|
});
|
||||||
|
this.highlightEls[this.highlightIndex].scrollIntoView({block: 'nearest', behavior: 'smooth'});
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.logs = [];
|
||||||
|
this.renderVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
export() {
|
||||||
|
const lines = this.logs.map(l => `${l.ts} [${l.level.toUpperCase()}] ${l.message}`);
|
||||||
|
const blob = new Blob([lines.join('\n')], {type: 'text/plain;charset=utf-8'});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `log_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.txt`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatTime(date) {
|
||||||
|
const t = date.toTimeString().slice(0, 8);
|
||||||
|
const ms = String(date.getMilliseconds()).padStart(3, '0');
|
||||||
|
return `${t}.${ms}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindResize() {
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
this.setContainerSize(); // 响应窗口变化
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bindGlobalEvents() {
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Escape' && this.searchInput) {
|
||||||
|
this.searchInput.value = '';
|
||||||
|
this.searchTerm = '';
|
||||||
|
this.renderVisible();
|
||||||
|
}
|
||||||
|
if (e.ctrlKey && e.key === 'k') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 快捷方法 */
|
||||||
|
info(msg, ts = this.formatTime(new Date())) {
|
||||||
|
this.log(msg, 'info', ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(msg, ts = this.formatTime(new Date())) {
|
||||||
|
this.log(msg, 'warn', ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(msg, ts = this.formatTime(new Date())) {
|
||||||
|
this.log(msg, 'error', ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
success(msg, ts = this.formatTime(new Date())) {
|
||||||
|
this.log(msg, 'success', ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(msg, ts = this.formatTime(new Date())) {
|
||||||
|
this.log(msg, 'debug', ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
system(msg, ts = this.formatTime(new Date())) {
|
||||||
|
this.log(msg, 'system', ts);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
/**
|
|
||||||
* WebSocket封装工具类
|
|
||||||
* <pre>
|
|
||||||
* 代码示例:
|
|
||||||
* let FmtSocket = new FmtSocket(
|
|
||||||
* {url:'websocket服务器地址',id:'唯一标识',timeout:心跳检测间隔时间(毫秒)}
|
|
||||||
* );
|
|
||||||
* //调用初始化方法
|
|
||||||
* FmtSocket.init((data) => {
|
|
||||||
* //初始化完成之后的回调函数 ata为监听到的数据
|
|
||||||
* //可以在这里做接收到消息后的逻辑处理
|
|
||||||
* console.log(data);
|
|
||||||
* });
|
|
||||||
* //调用发送消息方法 message 消息字符串或对象
|
|
||||||
* FmtSocket.sendMsg(message, () => {
|
|
||||||
* //发送消息成功之后的回调函数
|
|
||||||
* });
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
class FmtSocket {
|
|
||||||
id;
|
|
||||||
#options;
|
|
||||||
#ws;
|
|
||||||
#entity;
|
|
||||||
#deepProxyObj;
|
|
||||||
#proxyMsg;
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param {Object} options
|
|
||||||
*/
|
|
||||||
constructor(options) {
|
|
||||||
this.#options = this.#extend({
|
|
||||||
url: 'http://127.0.0.1/socket/ws',//后台websocket服务地址
|
|
||||||
id: this.#uuid(),//如果没传此参数,创建一个随机的UUID传给后台服务作为标识
|
|
||||||
timeout: 30000 //心跳检测间隔时间 默认30秒
|
|
||||||
}, options);
|
|
||||||
this.id = this.#options.id;
|
|
||||||
this.#ws = null;
|
|
||||||
//send:发送类型 HeartBeat:心跳 Logging:消息
|
|
||||||
this.#entity = {send: null, data: null};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化websocket
|
|
||||||
* @param {Function} callback (type, data) 回调函数
|
|
||||||
*/
|
|
||||||
init(callback){
|
|
||||||
if(callback && typeof callback === 'function'){
|
|
||||||
this.#proxyMsg = [];
|
|
||||||
this.#deepProxyObj = this.#deepProxy(this.#proxyMsg,callback);
|
|
||||||
}
|
|
||||||
this.#createWebSocket();
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 对象、数组变化监听(增删改)
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Function} callback
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
|
||||||
#deepProxy(obj, callback){
|
|
||||||
if (typeof obj === 'object') {
|
|
||||||
for (let key in obj) {
|
|
||||||
if (typeof obj[key] === 'object') {
|
|
||||||
obj[key] = this.#deepProxy(obj[key], callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Proxy(obj, {
|
|
||||||
/**
|
|
||||||
* @param {Object, Array} target 设置值的对象
|
|
||||||
* @param {String} key 属性
|
|
||||||
* @param {any} value 值
|
|
||||||
* @param {Object} receiver this
|
|
||||||
*/
|
|
||||||
set: (target, key, value, receiver)=> {
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
value = this.#deepProxy(value, callback);
|
|
||||||
}
|
|
||||||
let cbType = target[key] === undefined ? 'create' : 'modify';
|
|
||||||
//排除数组修改length回调
|
|
||||||
if (!(Array.isArray(target) && key === 'length')) {
|
|
||||||
if (cbType === 'create') {
|
|
||||||
callback(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Reflect.set(target, key, value, receiver);
|
|
||||||
},
|
|
||||||
deleteProperty(target, key) {
|
|
||||||
return Reflect.deleteProperty(target, key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
#createWebSocket(){
|
|
||||||
try {
|
|
||||||
if ('WebSocket' in window) {
|
|
||||||
this.#ws = new WebSocket((this.#options.url + "?id=" + this.#options.id)
|
|
||||||
.replace("http", "ws")
|
|
||||||
.replace("https", "wss"));
|
|
||||||
} else {
|
|
||||||
alert('浏览器不支持WebSocket通讯');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.#onopen();
|
|
||||||
this.#onmessage();
|
|
||||||
this.#onerror();
|
|
||||||
this.#onclose();
|
|
||||||
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
|
|
||||||
window.onbeforeunload = ()=> {
|
|
||||||
this.#ws.close();
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.log("WebSocket:链接失败", e);
|
|
||||||
alert('WebSocket:链接失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 关闭websocket链接
|
|
||||||
* @param {Function} callback
|
|
||||||
*/
|
|
||||||
closeWebSocket(callback){
|
|
||||||
if(this.#ws){
|
|
||||||
this.#ws.close();
|
|
||||||
}
|
|
||||||
if(callback && typeof callback === 'function'){
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 发送消息
|
|
||||||
* @param {String, Object} message
|
|
||||||
* @param {Function} callback
|
|
||||||
*/
|
|
||||||
sendMsg(message, callback){
|
|
||||||
if(!message || typeof message !== 'string' || typeof message !== 'object'){
|
|
||||||
alert("FmtSocket sendMsg message is not null or message is not a string or object");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if(callback && typeof callback !== 'function'){
|
|
||||||
alert("FmtSocket sendMsg callback is not a function");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.#emptyObjectPropertyValues(this.#entity);
|
|
||||||
this.#entity.send = 'Message';
|
|
||||||
this.#entity.data = message;
|
|
||||||
if (this.#ws.readyState === this.#ws.CONNECTING) {
|
|
||||||
// 正在开启状态,等待1s后重新调用
|
|
||||||
let socket = this;
|
|
||||||
setTimeout(function () {
|
|
||||||
socket.#ws.send(JSON.stringify(socket.#entity));
|
|
||||||
callback();
|
|
||||||
}, 1000)
|
|
||||||
}else{
|
|
||||||
this.#ws.send(JSON.stringify(this.#entity));
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#onopen(){
|
|
||||||
this.#ws.onopen = (event) => {
|
|
||||||
console.log("WebSocket:链接开启");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#onmessage(){
|
|
||||||
this.#ws.onmessage = (event) => {
|
|
||||||
this.#emptyObjectPropertyValues(this.#entity);
|
|
||||||
this.#entity = JSON.parse(event.data);
|
|
||||||
//心跳检测
|
|
||||||
if(this.#entity.send === 'HeartBeat'){
|
|
||||||
this.#heartBeat();
|
|
||||||
}
|
|
||||||
if(this.#deepProxyObj){
|
|
||||||
this.#deepProxyObj.splice(0);//赋值前先清空,保证消息数组里只有一条最新的数据
|
|
||||||
let data = JSON.parse(JSON.stringify(this.#entity));
|
|
||||||
this.#deepProxyObj.push(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#onerror(){
|
|
||||||
this.#ws.onerror = (event) => {
|
|
||||||
console.log("WebSocket:发生错误", event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#onclose(){
|
|
||||||
this.#ws.onclose = (event) => {
|
|
||||||
console.log("WebSocket:链接关闭");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#heartBeat(){
|
|
||||||
setTimeout(()=> {
|
|
||||||
this.#emptyObjectPropertyValues(this.#entity);
|
|
||||||
if (this.#ws.readyState === this.#ws.CLOSING
|
|
||||||
|| this.#ws.readyState === this.#ws.CLOSED){
|
|
||||||
alert("WebSocket通讯服务链接已关闭");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.#entity.send = 'HeartBeat';
|
|
||||||
this.#entity.data = '心跳检测';
|
|
||||||
if (this.#ws.readyState === this.#ws.CONNECTING) {
|
|
||||||
// 正在开启状态,等待1s后重新调用
|
|
||||||
setTimeout(()=> {
|
|
||||||
this.#ws.send(JSON.stringify(this.#entity));
|
|
||||||
}, 1000)
|
|
||||||
}else{
|
|
||||||
this.#ws.send(JSON.stringify(this.#entity));
|
|
||||||
}
|
|
||||||
},this.#options.timeout);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 扩展,source相同属性的值去覆盖target, target如果没有这个属性就新增
|
|
||||||
* @param {Object} target
|
|
||||||
* @param {Object} source
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
#extend(target, source){
|
|
||||||
if(typeof target !== 'object') return null;
|
|
||||||
if(typeof source !== 'object') return null;
|
|
||||||
for (const key in source) {
|
|
||||||
// 使用for in会遍历数组所有的可枚举属性,包括原型。
|
|
||||||
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
||||||
target[key] = source[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
#uuid(len, radix){
|
|
||||||
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
|
||||||
let uuid = [], i;
|
|
||||||
radix = radix || chars.length;
|
|
||||||
if (len) {
|
|
||||||
// Compact form
|
|
||||||
for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
|
|
||||||
} else {
|
|
||||||
// rfc4122, version 4 form
|
|
||||||
let r;
|
|
||||||
// rfc4122 requires these characters
|
|
||||||
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
|
|
||||||
uuid[14] = '4';
|
|
||||||
// Fill in random data. At i==19 set the high bits of clock sequence as
|
|
||||||
// per rfc4122, sec. 4.1.5
|
|
||||||
for (i = 0; i < 36; i++) {
|
|
||||||
if (!uuid[i]) {
|
|
||||||
r = 0 | Math.random() * 16;
|
|
||||||
uuid[i] = chars[(i === 19) ? (r & 0x3) | 0x8 : r];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return uuid.join('');
|
|
||||||
}
|
|
||||||
#emptyObjectPropertyValues(person){
|
|
||||||
Object.keys(person).forEach(key => (person[key] = null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<span class="ax-line"></span>
|
<span class="ax-line"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ax-item">
|
<div class="ax-item">
|
||||||
<a th:href="@{/logging/index}" class="ax-text">在线日志</a>
|
<a th:href="@{/logging/index}" target="_blank" class="ax-text">在线日志</a>
|
||||||
<span class="ax-line"></span>
|
<span class="ax-line"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ax-item">
|
<div class="ax-item">
|
||||||
|
|||||||
@@ -1,66 +1,72 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN" xmlns:th="https://www.thymeleaf.org">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no" />
|
<meta charset="UTF-8">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
|
||||||
<meta name="apple-touch-fullscreen" content="yes"/>
|
|
||||||
<meta name="format-detection" content="email=no" />
|
|
||||||
<meta name="wap-font-scale" content="no" />
|
|
||||||
<meta name="viewport" content="user-scalable=no, width=device-width" />
|
|
||||||
<meta content="telephone=no" name="format-detection" />
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
||||||
<title>在线日志</title>
|
<title>在线日志</title>
|
||||||
|
|
||||||
<link th:href="@{/axui-v2.1.1/css/ax.css}" rel="stylesheet" type="text/css" >
|
|
||||||
<link th:href="@{/axui-v2.1.1/css/ax-response.css}" rel="stylesheet" type="text/css" >
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="ax-header">
|
<!-- 日志容器 -->
|
||||||
<div class="ax-row">
|
<div id="logContainer"></div>
|
||||||
<div class="ax-col">
|
|
||||||
<a th:href="@{/download/index}" class="ax-logo">
|
|
||||||
<img th:src="@{/common/images/logo.png}" alt="File Management"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="ax-nav">
|
|
||||||
<div class="ax-item">
|
|
||||||
<a th:href="@{/download/index}" class="ax-text">文件列表</a>
|
|
||||||
<span class="ax-line"></span>
|
|
||||||
</div>
|
|
||||||
<div class="ax-item">
|
|
||||||
<a th:href="@{/upload/index}" class="ax-text">文件上传</a>
|
|
||||||
<span class="ax-line"></span>
|
|
||||||
</div>
|
|
||||||
<div class="ax-item ax-selected">
|
|
||||||
<a th:href="@{/logging/index}" class="ax-text">在线日志</a>
|
|
||||||
<span class="ax-line"></span>
|
|
||||||
</div>
|
|
||||||
<div class="ax-item">
|
|
||||||
<a th:href="@{/system/index}" class="ax-text">系统设置</a>
|
|
||||||
<span class="ax-line"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div class="ax-space-header"></div>
|
|
||||||
<div id="RootDiv" class="ax-border ax-margin">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<script th:src="@{/axui-v2.1.1/js/ax.js}" type="text/javascript" charset="utf-8"></script>
|
|
||||||
<script th:src="@{/common/js/basic.js}" type="text/javascript" charset="utf-8"></script>
|
<script th:src="@{/common/js/basic.js}" type="text/javascript" charset="utf-8"></script>
|
||||||
<script th:src="@{/common/js/fmt-socket.js}" type="text/javascript" charset="utf-8"></script>
|
<script th:src="@{/common/js/LogMonitorAdaptive.js}" type="text/javascript" charset="utf-8"></script>
|
||||||
<script type="text/javascript" th:inline="javascript" charset="utf-8">
|
<script type="text/javascript" th:inline="javascript" charset="utf-8">
|
||||||
window.onload = () => {
|
window.onload = () => {
|
||||||
const socket = new FmtSocket({url: Fmt.ctx() + '/socket/ws'});
|
|
||||||
socket.init((data) => {
|
let ws = null;
|
||||||
if (data.send === 'Logging') {
|
|
||||||
const log = data.data;
|
const logger = new LogMonitorAdaptive('#logContainer', {
|
||||||
const logDiv = document.createElement('div');
|
theme: 'dark',
|
||||||
logDiv.innerHTML = '<pre><strong>' + log + '</strong></pre>';
|
maxLines: 10000,
|
||||||
document.querySelector('#RootDiv').appendChild(logDiv);
|
fontSize: 14,
|
||||||
|
enableFilter: true,
|
||||||
|
enableSearch: true,
|
||||||
|
enableExport: true,
|
||||||
|
enableClear: true,
|
||||||
|
enablePause: true,
|
||||||
|
enableThemeToggle: true,
|
||||||
|
enableFullscreen: true,
|
||||||
|
enableFontSize: true,
|
||||||
|
enableWordWrap: true,
|
||||||
|
showTimestamp: true, // 是否显示时间戳
|
||||||
|
showLevel: true, // 是否显示日志级别标签
|
||||||
|
wordWrap: true, // 日志内容是否自动换行(true=换行,false=横向滚动)
|
||||||
|
//暂停/继续 回调函数
|
||||||
|
onTogglePause: async (isPaused) => {
|
||||||
|
const options = {
|
||||||
|
url: Fmt.ctx() + '/logging/close',
|
||||||
|
data: {closed: isPaused},
|
||||||
|
method: 'post'
|
||||||
|
};
|
||||||
|
await Fmt.axios(options).then((result) => console.log(result)).catch((err) => console.error(err));
|
||||||
|
ws.send('发送日志');
|
||||||
|
},
|
||||||
|
onCreated: () => {
|
||||||
|
console.log('日志容器已创建');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ('WebSocket' in window) {
|
||||||
|
ws = new WebSocket((Fmt.ctx() + '/socket/ws').replace("http", "ws").replace("https", "wss"));
|
||||||
|
} else {
|
||||||
|
alert('浏览器不支持WebSocket通讯');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = function (event) {
|
||||||
|
if(event.data){
|
||||||
|
let data = JSON.parse(event.data);
|
||||||
|
logger.log(data.loggerName + ' : ' + data.message, data.level, data.timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
|
||||||
|
window.onbeforeunload = ()=> {
|
||||||
|
ws.close();
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
alert('WebSocket:链接失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<span class="ax-line"></span>
|
<span class="ax-line"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ax-item">
|
<div class="ax-item">
|
||||||
<a th:href="@{/logging/index}" class="ax-text">在线日志</a>
|
<a th:href="@{/logging/index}" target="_blank" class="ax-text">在线日志</a>
|
||||||
<span class="ax-line"></span>
|
<span class="ax-line"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ax-item">
|
<div class="ax-item">
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package cn.somkit.fmt;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
|
|
||||||
@SpringBootTest
|
|
||||||
class FmtApplicationTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void contextLoads() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user