日志文件下载功能,支持多实例日志

This commit is contained in:
2026-01-29 19:36:12 +08:00
parent c9020ee8d3
commit 24b1967ba9
5 changed files with 609 additions and 256 deletions
@@ -1,256 +0,0 @@
package org.jeecg.modules.api.controller;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.log.ApiLog;
import org.jeecg.common.log.ErrorLog;
import org.jeecg.common.log.ErrorLogRepository;
import org.jeecg.common.log.TaskLog;
import org.jeecg.modules.api.bean.LogFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.http.*;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author Shunzhi Jiang
* @since 2023/10/13
*/
@RestController
@RequestMapping("/service/log")
@Slf4j
@Tag(name = "system/日志")
public class LogController {
@Autowired
private ElasticsearchOperations elasticsearchOperations;
@Autowired
private ErrorLogRepository errorLogRepository;
@Value("${logging.file.path:}")
private String loggingFilePath;
@Operation(description = "接口日志列表")
@PostMapping("/list")
@RequiresRoles("admin")
public Result<IPage<ApiLog>> pageList(@RequestBody LogFilter logFilter,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize) {
Pageable pageable = PageRequest.of(pageNo - 1, pageSize);
Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
BoolQueryBuilder mustQueryBuilder = QueryBuilders.boolQuery();
List<QueryBuilder> must = mustQueryBuilder.must();
if (StringUtils.hasText(logFilter.getService())) {
must.add(QueryBuilders.termQuery("service.keyword", logFilter.getService()));
}
if (Objects.nonNull(logFilter.getIsEx())) {
must.add(QueryBuilders.termQuery("isEx", logFilter.getIsEx()));
}
if (Objects.nonNull(logFilter.getStart()) && Objects.nonNull(logFilter.getEnd())) {
must.add(QueryBuilders.rangeQuery("createTime").gte(logFilter.getStart()).lte(logFilter.getEnd()));
}
if (Objects.nonNull(logFilter.getSpend())) {
must.add(QueryBuilders.rangeQuery("spend").gte(logFilter.getSpend()));
}
if (StringUtils.hasText(logFilter.getKeyword())) {
BoolQueryBuilder shouldQueryBuilder = QueryBuilders.boolQuery();
List<QueryBuilder> should = shouldQueryBuilder.should();
should.add(QueryBuilders.matchQuery("method", logFilter.getKeyword()));
should.add(QueryBuilders.matchQuery("requestUrl", logFilter.getKeyword()));
must.add(shouldQueryBuilder);
}
Query query = queryBuilder.withQuery(mustQueryBuilder)
.withPageable(pageable)
.withSort(sort)
.withTrackTotalHits(Boolean.TRUE)
.build();
SearchHits<ApiLog> search = elasticsearchOperations.search(query, ApiLog.class);
List<ApiLog> apiLogs = search.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
IPage<ApiLog> pageApiLog = new Page<>(pageNo, pageSize);
pageApiLog.setRecords(apiLogs);
pageApiLog.setTotal(search.getTotalHits());
return Result.ok(pageApiLog);
}
@Operation(description = "接口日志列表")
@PostMapping("/taskList")
@RequiresRoles("admin")
public Result<IPage<TaskLog>> taskList(@RequestBody LogFilter logFilter,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize) {
Pageable pageable = PageRequest.of(pageNo - 1, pageSize);
Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
BoolQueryBuilder mustQueryBuilder = QueryBuilders.boolQuery();
List<QueryBuilder> must = mustQueryBuilder.must();
if (StringUtils.hasText(logFilter.getService())) {
must.add(QueryBuilders.termQuery("service", logFilter.getService()));
}
if (StringUtils.hasText(logFilter.getTaskType())) {
must.add(QueryBuilders.termQuery("taskType", logFilter.getTaskType()));
}
if (StringUtils.hasText(logFilter.getTrigger())) {
must.add(QueryBuilders.termQuery("trigger", logFilter.getTrigger()));
}
if (Objects.nonNull(logFilter.getIsEx())) {
must.add(QueryBuilders.termQuery("isEx", logFilter.getIsEx()));
}
if (Objects.nonNull(logFilter.getStart()) && Objects.nonNull(logFilter.getEnd())) {
must.add(QueryBuilders.rangeQuery("createTime").gte(logFilter.getStart()).lte(logFilter.getEnd()));
}
if (Objects.nonNull(logFilter.getSpend())) {
must.add(QueryBuilders.rangeQuery("spend").gte(logFilter.getSpend()));
}
if (StringUtils.hasText(logFilter.getKeyword())) {
BoolQueryBuilder shouldQueryBuilder = QueryBuilders.boolQuery();
List<QueryBuilder> should = shouldQueryBuilder.should();
should.add(QueryBuilders.matchQuery("taskName", logFilter.getKeyword()));
should.add(QueryBuilders.matchQuery("method", logFilter.getKeyword()));
must.add(shouldQueryBuilder);
}
Query query = queryBuilder.withQuery(mustQueryBuilder)
.withPageable(pageable)
.withSort(sort)
.withTrackTotalHits(Boolean.TRUE)
.build();
SearchHits<TaskLog> search = elasticsearchOperations.search(query, TaskLog.class);
List<TaskLog> taskLogs = search.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
IPage<TaskLog> pageTaskLog = new Page<>(pageNo, pageSize);
pageTaskLog.setRecords(taskLogs);
pageTaskLog.setTotal(search.getTotalHits());
return Result.ok(pageTaskLog);
}
@Operation(description = "错误日志ID查询")
@GetMapping("/error/{id}")
@RequiresRoles("admin")
public Result<ErrorLog> getErrorLogById(@PathVariable String id) {
return Result.ok(errorLogRepository.findById(id).orElseThrow(() -> new RuntimeException("未找到错误日志")));
}
@Operation(description = "日志文件列表")
@GetMapping("/files")
@RequiresRoles("admin")
public Result<IPage<String>> pageFiles(String service,
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "10") Integer pageSize) {
Page<String> resultPage = Page.of(pageNo, pageSize);
if (!checkPathExist()) {
return Result.ok(resultPage);
}
File file = getLogPath(service).normalize().toFile();
File[] files = file.listFiles();
if (ArrayUtil.isEmpty(files)) {
return Result.ok(resultPage);
}
List<String> fileNames = Arrays.stream(files)
.filter(File::isFile)
.sorted(Comparator.comparing(this::getLastModifiedTime).reversed())
.map(File::getName)
.collect(Collectors.toList());
List<String> results = fileNames.subList(
getStartIndex(pageNo, pageSize, fileNames.size()),
getEndIndex(pageNo, pageSize, fileNames.size())
);
resultPage.setTotal(fileNames.size());
resultPage.setRecords(results);
return Result.ok(resultPage);
}
private int getStartIndex(Integer pageNo, Integer pageSize, Integer listSize) {
return Math.min(Math.max(0, pageNo - 1) * pageSize, Math.max(listSize, 0));
}
private int getEndIndex(Integer pageNo, Integer pageSize, Integer listSize) {
return Math.min(Math.max(1, pageNo) * pageSize, listSize);
}
private Long getLastModifiedTime(File file) {
try {
Path path = file.toPath();
BasicFileAttributes basicFileAttributes = Files.readAttributes(path, BasicFileAttributes.class);
return basicFileAttributes.lastModifiedTime().toMillis();
} catch (IOException e) {
return 0L;
}
}
private boolean checkPathExist() {
return StrUtil.isNotBlank(loggingFilePath);
}
private Path getLogPath(String service) {
String validService = validFilter(service);
return Paths.get(loggingFilePath).getParent().resolve(validService);
}
private String validFilter(String str) {
return StrUtil.strip(StrUtil.strip(StrUtil.replace(str, "..", ""), "/"), "\\");
}
@Operation(description = "日志文件下载")
@GetMapping("/file/down")
@RequiresRoles("admin")
public ResponseEntity<FileSystemResource> fileDown(String service, String filename) {
log.info("文件下载service:{}\tfilename:{}", service, filename);
try {
File file = getLogPath(service).resolve(filename).normalize().toFile();
if (file.exists()) {
log.info("下载日志【{}】", filename);
FileSystemResource resource = new FileSystemResource(file);
HttpHeaders headers = new HttpHeaders();
String name = URLEncoder.encode(file.getName(), "UTF-8");
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDisposition(ContentDisposition.attachment().filename(name).build()
);
return new ResponseEntity<>(resource, headers, HttpStatus.OK);
}
} catch (Exception e) {
log.error("下载文件出错service:{}\tfilename:{}", service, filename, e);
}
return null;
}
@Operation(description = "日志文件删除")
@GetMapping("/file/delete")
@RequiresRoles("admin")
public Result<Boolean> fileDelete(String service, String filename) throws IOException {
return Result.ok(Files.deleteIfExists(getLogPath(service).resolve(filename).normalize()));
}
}