本文摘要
介绍在泛微 E9 中通过自建 JSP 页面直接查看当前 ecology 日志的方法:将页面放入 ecology 目录后访问,带登录校验,可指定源编码与显示行数,读取 log/ecology 末尾日志并合并多行、统计日志级别。适合无 sysadmin 权限时快速排查测试环境问题,需注意日志文件路径与读取权限。
✅ 为什么?
- 无sysadmin限制,便于测试系统排查问题!
- 速度!快!
- 好看!
做一个xxxx.jsp,仍在ecology/目录下 访问你的OA地址:/xxxx.jsp
注: 丸子直接用cursor写的!
效果图

上代码
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %><%@ page import="weaver.hrm.User" %><%@ page import="weaver.hrm.HrmUserVarify" %><%@ page import="java.io.File" %><%@ page import="java.io.RandomAccessFile" %><%@ page import="java.io.IOException" %><%@ page import="java.util.ArrayList" %><%@ page import="java.util.List" %><%@ page import="java.util.Collections" %>
<%@ page import="weaver.hrm.User" %><%@ page import="weaver.hrm.HrmUserVarify" %><%@ page import="java.io.File" %><%@ page import="java.io.RandomAccessFile" %><%@ page import="java.io.IOException" %><%@ page import="java.util.ArrayList" %><%@ page import="java.util.List" %><%@ page import="java.util.Collections" %>
<%-- 1. 登录验证:未登录跳转到登录页 --%><% User user = HrmUserVarify.getUser(request, response); if (user == null) { response.sendRedirect("/login/Login.jsp"); return; }
// 方案A:页面与请求保持UTF-8 request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8");
// 源日志编码(默认GBK,可传GB2312/GB18030) String srcCharset = request.getParameter("srcCharset"); if (srcCharset == null || srcCharset.trim().isEmpty()) { srcCharset = "GBK"; }
// 2. 核心参数:当前ecology日志路径(固定路径,无需日期) String logDirPath = request.getRealPath("/") + "log" + File.separator; File logFile = new File(logDirPath + "ecology");
// 3. 显示行数设置:默认100行,限制10-1000行 String lineCountStr = request.getParameter("lineCount"); int lineCount = 100; if (lineCountStr != null && !lineCountStr.trim().isEmpty()) { try { lineCount = Integer.parseInt(lineCountStr.trim()); lineCount = Math.max(10, lineCount); lineCount = Math.min(1000, lineCount); } catch (NumberFormatException e) { lineCount = 100; } }
List<String> logContent = new ArrayList<>(); String errorMsg = "";
// 日志级别统计 int infoCount = 0, errorCount = 0, warnCount = 0, debugCount = 0;
if ("POST".equalsIgnoreCase(request.getMethod())) { try { if (!logFile.exists()) { errorMsg = "日志文件不存在:" + logFile.getAbsolutePath(); } else if (!logFile.canRead()) { errorMsg = "无权限读取日志文件:" + logFile.getAbsolutePath(); } else { // 使用源编码读取(GB2312/GBK/GB18030等) List<String> rawLogContent = readLastNLines(logFile, lineCount, srcCharset); // 合并多行日志并统计日志级别 StringBuilder currentLog = new StringBuilder(); String currentLevel = "info";
for (String line : rawLogContent) { if (line == null || line.replaceAll("[ \t\u3000]", "").trim().isEmpty()) { continue; }
// 检查是否是新的日志条目(以时间戳开头) if (isNewLogEntry(line)) { // 保存之前的日志条目 if (currentLog.length() > 0) { logContent.add(currentLog.toString()); // 统计日志级别 switch (currentLevel) { case "info": infoCount++; break; case "error": errorCount++; break; case "warn": warnCount++; break; case "debug": debugCount++; break; } } // 开始新的日志条目 currentLog = new StringBuilder(line); currentLevel = parseLogLevel(line); } else { // 继续当前日志条目 currentLog.append("\n").append(line); } }
// 处理最后一个日志条目 if (currentLog.length() > 0) { logContent.add(currentLog.toString()); switch (currentLevel) { case "info": infoCount++; break; case "error": errorCount++; break; case "warn": warnCount++; break; case "debug": debugCount++; break; } } } } catch (IOException e) { errorMsg = "读取日志失败:" + e.getMessage(); e.printStackTrace(); } }%>
<%-- 6. 自定义方法:必须放在 <%! %> 声明块中(JSP语法要求) --%><%! /** * 读取文件的最后N行,按指定源编码解码成Unicode */ private List<String> readLastNLines(File file, int n, String charsetName) throws IOException { List<String> result = new ArrayList<>(); if (n <= 0) return result;
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { long fileLength = raf.length(); long pos = fileLength - 1; int lines = 0; java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(256);
while (pos >= 0 && lines < n) { raf.seek(pos); int b = raf.readByte() & 0xFF;
if (b == 0x0A && pos != fileLength - 1) { // \n byte[] bytes = baos.toByteArray(); reverseBytes(bytes); result.add(new String(bytes, charsetName)); baos.reset(); lines++; } else if (b != 0x0D && b != 0x0A) { baos.write(b); }
pos--; }
if (baos.size() > 0) { byte[] bytes = baos.toByteArray(); reverseBytes(bytes); result.add(new String(bytes, charsetName)); }
Collections.reverse(result); } return result; }
private void reverseBytes(byte[] arr) { int i = 0, j = arr.length - 1; while (i < j) { byte tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; i++; j--; } }
/** * 解析日志级别 */ private String parseLogLevel(String logLine) { if (logLine == null) return "info"; String upperLine = logLine.toUpperCase(); if (upperLine.contains(" ERROR ")) return "error"; if (upperLine.contains(" WARN ")) return "warn"; if (upperLine.contains(" DEBUG ")) return "debug"; if (upperLine.contains(" INFO ")) return "info"; return "info"; }
/** * 解析日志时间戳 */ private String parseLogTimestamp(String logLine) { if (logLine == null) return ""; // 匹配格式:2025-10-17 14:01:34,407 java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3})"); java.util.regex.Matcher matcher = pattern.matcher(logLine); if (matcher.find()) { return matcher.group(1); } return ""; }
/** * 解析线程信息 */ private String parseLogThread(String logLine) { if (logLine == null) return ""; // 匹配格式:[null] Thread-119-239[weaver.email.timer.MailClearTimer:92] java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\[([^\\]]+)\\]\\s+([^\\[]+)\\[([^\\]]+)\\]"); java.util.regex.Matcher matcher = pattern.matcher(logLine); if (matcher.find()) { return matcher.group(2) + "[" + matcher.group(3) + "]"; } return ""; }
/** * 解析日志消息内容 */ private String parseLogMessage(String logLine) { if (logLine == null) return ""; // 找到最后一个 " - " 后的内容 int lastDash = logLine.lastIndexOf(" - "); if (lastDash != -1 && lastDash + 3 < logLine.length()) { return logLine.substring(lastDash + 3); } return logLine; }
/** * 判断是否是新的日志条目(以时间戳开头) */ private boolean isNewLogEntry(String line) { if (line == null || line.trim().isEmpty()) return false; // 匹配格式:2025-10-17 14:01:34,407 INFO return line.matches("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3}.*"); }%>
<!DOCTYPE html><html lang="zh-CN">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>当前Ecology日志查看</title> <!-- 简化样式:不依赖外部CDN,避免加载失败 --> <style> body { margin: 0; padding: 15px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #fafbfc; color: #333; }
.container { max-width: 1400px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); border: 1px solid #e1e4e8; }
.title-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; border-bottom: 1px solid #e1e4e8; padding-bottom: 8px; }
.title { font-size: 20px; font-weight: 600; color: #24292e; }
.user-info { color: #586069; font-size: 14px; }
.form-group { margin-bottom: 15px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.form-group label { font-size: 14px; color: #586069; font-weight: 500; }
.form-group input, .form-group select { padding: 8px 12px; border: 1px solid #d1d5da; border-radius: 6px; font-size: 14px; background: white; }
.form-group input { width: 120px; }
.form-group button { padding: 8px 16px; background: #28a745; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; }
.form-group button:hover { background: #218838; }
.log-box { border: 1px solid #e1e4e8; border-radius: 8px; overflow: hidden; background: white; }
.log-header { padding: 12px 16px; background: #f6f8fa; border-bottom: 1px solid #e1e4e8; font-size: 14px; color: #586069; }
.log-content { padding: 12px; max-height: 700px; overflow-y: auto; font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; font-size: 13px; line-height: 1.5; background: white; }
.log-entry { margin-bottom: 6px; padding: 10px 12px; border-radius: 6px; border-left: 3px solid transparent; background: #f8f9fa; word-break: break-all; white-space: normal; font-size: 13px; line-height: 1.4; }
.log-entry:hover { background: #f1f3f4; }
.log-entry.info { border-left-color: #0366d6; background: #f1f8ff; }
.log-entry.error { border-left-color: #d73a49; background: #ffeef0; }
.log-entry.warn { border-left-color: #e36209; background: #fff8f0; }
.log-entry.debug { border-left-color: #6a737d; background: #f6f8fa; }
.log-message { color: #24292e; }
.log-stats { display: flex; gap: 16px; margin-bottom: 12px; padding: 8px 12px; background: #f6f8fa; font-size: 13px; }
.log-stats .stat-item { display: flex; align-items: center; gap: 4px; }
.log-stats .stat-label { color: #586069; font-weight: 500; }
.log-stats .stat-value { font-weight: 600; font-size: 14px; }
.log-stats .stat-value.info { color: #0366d6; }
.log-stats .stat-value.error { color: #d73a49; }
.log-stats .stat-value.warn { color: #e36209; }
.log-stats .stat-value.debug { color: #6a737d; }
.error-msg { padding: 12px 16px; color: #d73a49; background: #ffeef0; border: 1px solid #fdaeb7; border-radius: 6px; font-size: 14px; }
.empty-msg { padding: 40px; text-align: center; color: #6a737d; font-size: 14px; } </style></head>
<body> <div class="container"> <!-- 标题和登录用户信息 --> <div class="title-bar"> <div class="title">当前Ecology日志查看</div> <div class="user-info">登录用户:<%= user.getLoginid() %></div> </div>
<!-- 行数设置表单 --> <form method="post" class="form-group"> <label for="lineCount">显示最新行数:</label> <input type="number" id="lineCount" name="lineCount" value="<%= lineCount %>" min="10" max="1000" step="10" required> <span style="margin: 0 10px; font-size: 12px; color: #999;">(10-1000行)</span>
<label for="srcCharset" style="margin-left: 20px;">源编码:</label> <select id="srcCharset" name="srcCharset"> <option value="UTF-8" <%= "UTF-8".equalsIgnoreCase(srcCharset) ? "selected" : "" %>>UTF-8</option> <option value="GBK" <%= "GBK".equalsIgnoreCase(srcCharset) ? "selected" : "" %>>GBK</option> <option value="GB2312" <%= "GB2312".equalsIgnoreCase(srcCharset) ? "selected" : "" %>>GB2312</option> <option value="GB18030" <%= "GB18030".equalsIgnoreCase(srcCharset) ? "selected" : "" %>>GB18030</option> </select>
<button type="submit" style="margin-left: 12px;">加载日志</button> </form>
<!-- 日志显示区域 --> <div class="log-box"> <div class="log-header"> 日志文件路径:<%= logFile.getAbsolutePath() %> <% if (!logContent.isEmpty()) { %>| 实际显示行数:<%= logContent.size() %><% } %> | 源编码:<%= srcCharset %> </div>
<% if (!logContent.isEmpty()) { %> <!-- 日志统计信息 --> <div class="log-stats"> <div class="stat-item"> <span class="stat-label">INFO:</span> <span class="stat-value info"><%= infoCount %></span> </div> <div class="stat-item"> <span class="stat-label">ERROR:</span> <span class="stat-value error"><%= errorCount %></span> </div> <div class="stat-item"> <span class="stat-label">WARN:</span> <span class="stat-value warn"><%= warnCount %></span> </div> <div class="stat-item"> <span class="stat-label">DEBUG:</span> <span class="stat-value debug"><%= debugCount %></span> </div> </div>
<% } %>
<%-- 错误提示 --%> <% if (!errorMsg.isEmpty()) { %> <div class="error-msg"><%= errorMsg %></div> <%-- 空状态(未点击加载) --%> <% } else if (logContent.isEmpty()) { %> <div class="empty-msg">请设置行数并点击"加载日志"按钮查看内容</div> <%-- 日志内容 --%> <% } else { %> <div class="log-content"> <% for (int i = logContent.size() - 1; i >= 0; i--) { String logEntry = logContent.get(i); if (logEntry == null || logEntry.trim().isEmpty()) continue;
// 解析日志各部分(从第一行获取) String[] lines = logEntry.split("\n"); String firstLine = lines[0]; String logLevel = parseLogLevel(firstLine); String timestamp = parseLogTimestamp(firstLine); String thread = parseLogThread(firstLine);
// 清理日志内容,正确处理多行日志 String cleanedLogEntry = logEntry.replaceAll("\\r\\n|\\r|\\n", " ") // 将换行符替换为空格 .replaceAll("\\s+", " ") // 将多个连续空白字符替换为单个空格 .trim(); // 去除首尾空白
// HTML转义整个日志内容 String escapedLogEntry = cleanedLogEntry.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); %> <div class="log-entry <%= logLevel %>"> <div class="log-message"><%= escapedLogEntry %></div> </div> <% } %> </div> <% } %> </div> </div></body>
</html>