| | |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import jakarta.servlet.http.HttpServletResponse; |
| | | import java.io.BufferedWriter; |
| | | import java.io.OutputStreamWriter; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.nio.file.Files; |
| | | import java.nio.file.Path; |
| | | import java.nio.file.Paths; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.time.LocalTime; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.time.format.DateTimeParseException; |
| | | import java.util.*; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.regex.Matcher; |
| | | import java.util.regex.Pattern; |
| | | import java.util.stream.Collectors; |
| | | import java.util.stream.Stream; |
| | | import java.util.zip.ZipEntry; |
| | |
| | | |
| | | @Value("${deviceLogStorage.loggingPath}") |
| | | private String loggingPath; |
| | | |
| | | @Value("${logging.file.path:./stock/out/@pom.build.finalName@/logs}") |
| | | private String systemLoggingPath; |
| | | |
| | | private static final DateTimeFormatter SYSTEM_LOG_DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd"); |
| | | private static final DateTimeFormatter SYSTEM_LOG_DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); |
| | | private static final DateTimeFormatter SYSTEM_LOG_EXPORT_TIME = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"); |
| | | private static final Pattern SYSTEM_LOG_TIMESTAMP = Pattern.compile("^(\\d{2}:\\d{2}:\\d{2}\\.\\d{3})"); |
| | | private static final Pattern SYSTEM_LOG_ROLLED_FILE = Pattern.compile("^(info|error)_(\\d{4}-\\d{2}-\\d{2})\\.(\\d+)\\.log$"); |
| | | |
| | | private static class ProgressInfo { |
| | | long totalRaw; |
| | |
| | | Path lastFile; |
| | | } |
| | | |
| | | private static class SystemLogFileInfo { |
| | | String logType; |
| | | LocalDate day; |
| | | Integer index; |
| | | boolean active; |
| | | Path path; |
| | | } |
| | | |
| | | private static class SystemLogDownloadRequest { |
| | | String logType; |
| | | LocalDateTime startTime; |
| | | LocalDateTime endTime; |
| | | List<Path> files; |
| | | } |
| | | |
| | | private static final Map<String, ProgressInfo> DOWNLOAD_PROGRESS = new ConcurrentHashMap<>(); |
| | | private static final int SYSTEM_LOG_MAX_RANGE_DAYS = 10; |
| | | private static final int SYSTEM_LOG_MATCH_BUFFER_LINES = 200000; |
| | | private static final String SYSTEM_LOG_ENTRY_NAME = "system.log"; |
| | | |
| | | private static final Map<String, SystemLogDownloadRequest> SYSTEM_LOG_DOWNLOAD_REQUESTS = new ConcurrentHashMap<>(); |
| | | |
| | | @RequestMapping(value = "/deviceLog/dates/auth") |
| | | @ManagerAuth |
| | |
| | | return R.ok(res); |
| | | } |
| | | |
| | | @RequestMapping(value = "/deviceLog/system/download/init/auth") |
| | | @ManagerAuth |
| | | public R initSystemDownload(@org.springframework.web.bind.annotation.RequestBody com.alibaba.fastjson.JSONObject param) { |
| | | try { |
| | | String logType = normalizeSystemLogType(param.getString("logType")); |
| | | if (logType == null) { |
| | | return R.error("日志类型错误"); |
| | | } |
| | | LocalDateTime startTime = parseSystemLogDateTime(param.getString("startTime")); |
| | | LocalDateTime endTime = parseSystemLogDateTime(param.getString("endTime")); |
| | | if (startTime == null || endTime == null) { |
| | | return R.error("时间格式错误"); |
| | | } |
| | | if (startTime.isAfter(endTime)) { |
| | | return R.error("开始时间不能晚于结束时间"); |
| | | } |
| | | long daySpan = java.time.temporal.ChronoUnit.DAYS.between(startTime.toLocalDate(), endTime.toLocalDate()); |
| | | if (daySpan > SYSTEM_LOG_MAX_RANGE_DAYS) { |
| | | return R.error("时间范围不能超过" + SYSTEM_LOG_MAX_RANGE_DAYS + "天"); |
| | | } |
| | | List<Path> files = findSystemLogFiles(logType, startTime, endTime); |
| | | if (files.isEmpty()) { |
| | | return R.error("未找到日志文件"); |
| | | } |
| | | String id = UUID.randomUUID().toString(); |
| | | ProgressInfo info = new ProgressInfo(); |
| | | info.totalCount = files.size(); |
| | | long sum = 0L; |
| | | for (Path f : files) { |
| | | try { sum += Files.size(f); } catch (Exception ignored) {} |
| | | } |
| | | info.totalRaw = sum; |
| | | info.processedRaw = 0L; |
| | | info.processedCount = 0; |
| | | info.finished = false; |
| | | DOWNLOAD_PROGRESS.put(id, info); |
| | | SystemLogDownloadRequest request = new SystemLogDownloadRequest(); |
| | | request.logType = logType; |
| | | request.startTime = startTime; |
| | | request.endTime = endTime; |
| | | request.files = files; |
| | | SYSTEM_LOG_DOWNLOAD_REQUESTS.put(id, request); |
| | | Map<String, Object> res = new HashMap<>(); |
| | | res.put("progressId", id); |
| | | res.put("totalSize", info.totalRaw); |
| | | res.put("fileCount", info.totalCount); |
| | | return R.ok(res); |
| | | } catch (Exception e) { |
| | | log.error("初始化系统日志下载失败", e); |
| | | return R.error("初始化失败"); |
| | | } |
| | | } |
| | | |
| | | @RequestMapping(value = "/deviceLog/system/download/auth") |
| | | @ManagerAuth |
| | | public void downloadSystemLog(@RequestParam("logType") String logTypeParam, |
| | | @RequestParam("startTime") String startTimeParam, |
| | | @RequestParam("endTime") String endTimeParam, |
| | | @RequestParam(value = "progressId", required = false) String progressId, |
| | | HttpServletResponse response) { |
| | | String progressKey = null; |
| | | try { |
| | | String logType = normalizeSystemLogType(logTypeParam); |
| | | LocalDateTime startTime = parseSystemLogDateTime(startTimeParam); |
| | | LocalDateTime endTime = parseSystemLogDateTime(endTimeParam); |
| | | if (logType == null || startTime == null || endTime == null || startTime.isAfter(endTime)) { |
| | | response.setStatus(400); |
| | | return; |
| | | } |
| | | SystemLogDownloadRequest requestInfo = null; |
| | | if (!Cools.isEmpty(progressId)) { |
| | | requestInfo = SYSTEM_LOG_DOWNLOAD_REQUESTS.get(progressId); |
| | | progressKey = progressId; |
| | | } |
| | | List<Path> files; |
| | | if (requestInfo != null |
| | | && Objects.equals(requestInfo.logType, logType) |
| | | && Objects.equals(requestInfo.startTime, startTime) |
| | | && Objects.equals(requestInfo.endTime, endTime)) { |
| | | files = requestInfo.files == null ? Collections.emptyList() : requestInfo.files; |
| | | } else { |
| | | files = findSystemLogFiles(logType, startTime, endTime); |
| | | } |
| | | if (files.isEmpty()) { |
| | | response.setStatus(404); |
| | | return; |
| | | } |
| | | if (Cools.isEmpty(progressKey)) { |
| | | progressKey = UUID.randomUUID().toString(); |
| | | } |
| | | ProgressInfo info = prepareProgress(progressKey, files); |
| | | response.reset(); |
| | | response.setContentType("application/zip"); |
| | | String filename = logType + "_" + formatSystemExportTime(startTime) + "_" + formatSystemExportTime(endTime) + ".zip"; |
| | | response.setHeader("Content-Disposition", "attachment; filename=" + filename); |
| | | response.setHeader("X-Progress-Id", progressKey); |
| | | try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) { |
| | | zos.putNextEntry(new ZipEntry(SYSTEM_LOG_ENTRY_NAME)); |
| | | try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(zos, StandardCharsets.UTF_8))) { |
| | | boolean written = writeSystemLogContent(files, logType, startTime, endTime, writer, info); |
| | | writer.flush(); |
| | | if (!written) { |
| | | response.reset(); |
| | | response.setStatus(404); |
| | | return; |
| | | } |
| | | } |
| | | zos.closeEntry(); |
| | | zos.finish(); |
| | | info.finished = true; |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("下载系统日志失败", e); |
| | | try { |
| | | response.reset(); |
| | | response.setStatus(500); |
| | | } catch (Exception ignore) { |
| | | } |
| | | } finally { |
| | | if (!Cools.isEmpty(progressId)) { |
| | | SYSTEM_LOG_DOWNLOAD_REQUESTS.remove(progressId); |
| | | } |
| | | } |
| | | } |
| | | |
| | | @RequestMapping(value = "/deviceLog/enums/auth") |
| | | @ManagerAuth |
| | | public R getEnums() { |
| | |
| | | return R.ok(enums); |
| | | } |
| | | |
| | | private ProgressInfo prepareProgress(String progressId, List<Path> files) { |
| | | return DOWNLOAD_PROGRESS.compute(progressId, (key, existing) -> { |
| | | ProgressInfo next = existing == null ? new ProgressInfo() : existing; |
| | | next.totalCount = files == null ? 0 : files.size(); |
| | | long total = 0L; |
| | | if (files != null) { |
| | | for (Path f : files) { |
| | | try { total += Files.size(f); } catch (Exception ignored) {} |
| | | } |
| | | } |
| | | next.totalRaw = total; |
| | | next.processedRaw = 0L; |
| | | next.processedCount = 0; |
| | | next.finished = false; |
| | | return next; |
| | | }); |
| | | } |
| | | |
| | | private boolean writeSystemLogContent(List<Path> files, |
| | | String logType, |
| | | LocalDateTime startTime, |
| | | LocalDateTime endTime, |
| | | BufferedWriter writer, |
| | | ProgressInfo progressInfo) throws Exception { |
| | | boolean wroteAny = false; |
| | | for (Path file : files) { |
| | | SystemLogFileInfo fileInfo = parseSystemLogFile(file); |
| | | if (fileInfo == null) { |
| | | updateProgress(progressInfo, file); |
| | | continue; |
| | | } |
| | | LocalDate baseDate = fileInfo.day != null ? fileInfo.day : startTime.toLocalDate(); |
| | | try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) { |
| | | List<String> pending = new ArrayList<>(); |
| | | boolean pendingMatched = false; |
| | | for (Iterator<String> iterator = lines.iterator(); iterator.hasNext(); ) { |
| | | String line = iterator.next(); |
| | | Matcher matcher = SYSTEM_LOG_TIMESTAMP.matcher(line == null ? "" : line); |
| | | if (matcher.find()) { |
| | | if (pendingMatched && !pending.isEmpty()) { |
| | | for (String pendingLine : pending) { |
| | | writer.write(pendingLine == null ? "" : pendingLine); |
| | | writer.newLine(); |
| | | } |
| | | wroteAny = true; |
| | | } |
| | | pending.clear(); |
| | | pendingMatched = isSystemLogLineInRange(baseDate, matcher.group(1), startTime, endTime); |
| | | } |
| | | if (pendingMatched) { |
| | | pending.add(line); |
| | | if (pending.size() > SYSTEM_LOG_MATCH_BUFFER_LINES) { |
| | | for (String pendingLine : pending) { |
| | | writer.write(pendingLine == null ? "" : pendingLine); |
| | | writer.newLine(); |
| | | } |
| | | wroteAny = true; |
| | | pending.clear(); |
| | | } |
| | | } else if (!pending.isEmpty()) { |
| | | pending.clear(); |
| | | } |
| | | } |
| | | if (pendingMatched && !pending.isEmpty()) { |
| | | for (String pendingLine : pending) { |
| | | writer.write(pendingLine == null ? "" : pendingLine); |
| | | writer.newLine(); |
| | | } |
| | | wroteAny = true; |
| | | } |
| | | } |
| | | updateProgress(progressInfo, file); |
| | | } |
| | | if (progressInfo != null) { |
| | | progressInfo.finished = true; |
| | | } |
| | | return wroteAny; |
| | | } |
| | | |
| | | private void updateProgress(ProgressInfo progressInfo, Path file) { |
| | | if (progressInfo == null) { |
| | | return; |
| | | } |
| | | try { |
| | | progressInfo.processedRaw += Files.size(file); |
| | | } catch (Exception ignored) { |
| | | } |
| | | progressInfo.processedCount += 1; |
| | | } |
| | | |
| | | private List<Path> findSystemLogFiles(String logType, LocalDateTime startTime, LocalDateTime endTime) throws Exception { |
| | | Path baseDir = Paths.get(systemLoggingPath); |
| | | if (!Files.exists(baseDir) || !Files.isDirectory(baseDir)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | LocalDate startDate = startTime.toLocalDate(); |
| | | LocalDate endDate = endTime.toLocalDate(); |
| | | List<SystemLogFileInfo> matched = new ArrayList<>(); |
| | | try (Stream<Path> stream = Files.list(baseDir)) { |
| | | stream.filter(path -> !Files.isDirectory(path)).forEach(path -> { |
| | | SystemLogFileInfo info = parseSystemLogFile(path); |
| | | if (info == null || !Objects.equals(info.logType, logType)) { |
| | | return; |
| | | } |
| | | if (info.active || info.day == null || (!info.day.isBefore(startDate) && !info.day.isAfter(endDate))) { |
| | | matched.add(info); |
| | | } |
| | | }); |
| | | } |
| | | matched.sort((left, right) -> { |
| | | LocalDate leftDay = left.day == null ? LocalDate.MAX : left.day; |
| | | LocalDate rightDay = right.day == null ? LocalDate.MAX : right.day; |
| | | int cmp = leftDay.compareTo(rightDay); |
| | | if (cmp != 0) { |
| | | return cmp; |
| | | } |
| | | int leftIndex = left.index == null ? Integer.MAX_VALUE : left.index; |
| | | int rightIndex = right.index == null ? Integer.MAX_VALUE : right.index; |
| | | if (left.active != right.active) { |
| | | return left.active ? 1 : -1; |
| | | } |
| | | return Integer.compare(leftIndex, rightIndex); |
| | | }); |
| | | return matched.stream().map(item -> item.path).collect(Collectors.toList()); |
| | | } |
| | | |
| | | private SystemLogFileInfo parseSystemLogFile(Path path) { |
| | | if (path == null) { |
| | | return null; |
| | | } |
| | | String name = path.getFileName().toString(); |
| | | if ("info.log".equals(name) || "error.log".equals(name)) { |
| | | SystemLogFileInfo info = new SystemLogFileInfo(); |
| | | info.logType = name.startsWith("info") ? "info" : "error"; |
| | | info.active = true; |
| | | info.path = path; |
| | | return info; |
| | | } |
| | | Matcher matcher = SYSTEM_LOG_ROLLED_FILE.matcher(name); |
| | | if (!matcher.matches()) { |
| | | return null; |
| | | } |
| | | try { |
| | | SystemLogFileInfo info = new SystemLogFileInfo(); |
| | | info.logType = matcher.group(1); |
| | | info.day = LocalDate.parse(matcher.group(2), SYSTEM_LOG_DATE); |
| | | info.index = Integer.parseInt(matcher.group(3)); |
| | | info.active = false; |
| | | info.path = path; |
| | | return info; |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private String normalizeSystemLogType(String logType) { |
| | | if (Cools.isEmpty(logType)) { |
| | | return null; |
| | | } |
| | | String value = logType.trim().toLowerCase(Locale.ROOT); |
| | | if ("info".equals(value) || "error".equals(value)) { |
| | | return value; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private LocalDateTime parseSystemLogDateTime(String value) { |
| | | if (Cools.isEmpty(value)) { |
| | | return null; |
| | | } |
| | | try { |
| | | return LocalDateTime.parse(value.trim(), SYSTEM_LOG_DATE_TIME); |
| | | } catch (DateTimeParseException e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private boolean isSystemLogLineInRange(LocalDate baseDate, |
| | | String timePart, |
| | | LocalDateTime startTime, |
| | | LocalDateTime endTime) { |
| | | if (baseDate == null || Cools.isEmpty(timePart) || startTime == null || endTime == null) { |
| | | return false; |
| | | } |
| | | try { |
| | | LocalTime time = LocalTime.parse(timePart); |
| | | LocalDateTime timestamp = LocalDateTime.of(baseDate, time); |
| | | return !timestamp.isBefore(startTime) && !timestamp.isAfter(endTime); |
| | | } catch (DateTimeParseException e) { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | private String formatSystemExportTime(LocalDateTime dateTime) { |
| | | if (dateTime == null) { |
| | | return "unknown"; |
| | | } |
| | | return SYSTEM_LOG_EXPORT_TIME.format(dateTime); |
| | | } |
| | | |
| | | |
| | | private Map<String, Object> buildEmptySummary() { |
| | | return buildSummaryResponse(Collections.emptyList()); |
| | | } |