为什么?
- 无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>
[泛微 EC9] GETE9LOG.jsp 直接查看当前E9日志 by https://oneszhang.com/archives/169.html