MENU

[泛微 EC9] GETE9LOG.jsp 直接查看当前E9日志

• 2025 年 10 月 17 日 • 阅读: 99 • OA

为什么?

  1. 无sysadmin限制,便于测试系统排查问题!
  2. 速度!快!
  3. 好看!

怎么用?

做一个xxxx.jsp,仍在ecology/目录下
访问你的OA地址:/xxxx.jsp

注: 丸子直接用cursor写的!

效果图

image.png

上代码

<%@ 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("&", "&amp;")
                                                          .replace("<", "&lt;")
                                                          .replace(">", "&gt;")
                                                          .replace("\"", "&quot;")
                                                          .replace("'", "&#39;");
                %>
                <div class="log-entry <%= logLevel %>">
                    <div class="log-message"><%= escapedLogEntry %></div>
                </div>
                <% } %>
            </div>
            <% } %>
        </div>
    </div>
</body>

</html>