Tomcat 日志配置与管理
Logging Configuration and Management
概述
日志系统是Tomcat运维和故障排除的重要工具。本文详细介绍Tomcat日志配置、日志级别管理、日志轮转、日志分析和监控技术。
1. Tomcat日志系统架构
1.1 日志组件架构
Tomcat日志系统
├── JULI (Java Util Logging Implementation)
│ ├── ClassLoaderLogManager
│ ├── DirectJDKLog
│ └── FileHandler
├── 日志级别
│ ├── SEVERE (严重)
│ ├── WARNING (警告)
│ ├── INFO (信息)
│ ├── CONFIG (配置)
│ ├── FINE (详细)
│ ├── FINER (更详细)
│ └── FINEST (最详细)
└── 日志文件
├── catalina.out
├── catalina.YYYY-MM-DD.log
├── localhost.YYYY-MM-DD.log
├── manager.YYYY-MM-DD.log
└── host-manager.YYYY-MM-DD.log
1.2 默认日志配置
# conf/logging.properties - 默认日志配置
# 全局处理器配置
handlers = 1catalina.org.apache.juli.AsyncFileHandler, 2localhost.org.apache.juli.AsyncFileHandler, 3manager.org.apache.juli.AsyncFileHandler, 4host-manager.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler
.handlers = 1catalina.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler
# 根日志级别
.level = INFO
# Catalina日志处理器
1catalina.org.apache.juli.AsyncFileHandler.level = FINE
1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina.
1catalina.org.apache.juli.AsyncFileHandler.maxDays = 90
1catalina.org.apache.juli.AsyncFileHandler.encoding = UTF-8
# Localhost日志处理器
2localhost.org.apache.juli.AsyncFileHandler.level = FINE
2localhost.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
2localhost.org.apache.juli.AsyncFileHandler.prefix = localhost.
2localhost.org.apache.juli.AsyncFileHandler.maxDays = 90
2localhost.org.apache.juli.AsyncFileHandler.encoding = UTF-8
# Manager应用日志
3manager.org.apache.juli.AsyncFileHandler.level = FINE
3manager.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
3manager.org.apache.juli.AsyncFileHandler.prefix = manager.
3manager.org.apache.juli.AsyncFileHandler.maxDays = 90
3manager.org.apache.juli.AsyncFileHandler.encoding = UTF-8
# Console处理器
java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = org.apache.juli.OneLineFormatter
java.util.logging.ConsoleHandler.encoding = UTF-8
2. 高级日志配置
2.1 自定义日志配置
# conf/logging.properties - 生产环境优化配置
# 处理器定义
handlers = 1catalina.org.apache.juli.AsyncFileHandler, 2localhost.org.apache.juli.AsyncFileHandler, 3access.org.apache.juli.AsyncFileHandler, 4error.org.apache.juli.AsyncFileHandler
.handlers = 1catalina.org.apache.juli.AsyncFileHandler
# 全局日志级别
.level = INFO
# Catalina主日志
1catalina.org.apache.juli.AsyncFileHandler.level = INFO
1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina.
1catalina.org.apache.juli.AsyncFileHandler.suffix = .log
1catalina.org.apache.juli.AsyncFileHandler.maxDays = 30
1catalina.org.apache.juli.AsyncFileHandler.encoding = UTF-8
1catalina.org.apache.juli.AsyncFileHandler.rotatable = true
1catalina.org.apache.juli.AsyncFileHandler.bufferSize = 16384
# Localhost日志
2localhost.org.apache.juli.AsyncFileHandler.level = WARNING
2localhost.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
2localhost.org.apache.juli.AsyncFileHandler.prefix = localhost.
2localhost.org.apache.juli.AsyncFileHandler.maxDays = 15
2localhost.org.apache.juli.AsyncFileHandler.encoding = UTF-8
# 访问日志
3access.org.apache.juli.AsyncFileHandler.level = INFO
3access.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
3access.org.apache.juli.AsyncFileHandler.prefix = access.
3access.org.apache.juli.AsyncFileHandler.maxDays = 7
3access.org.apache.juli.AsyncFileHandler.encoding = UTF-8
# 错误日志
4error.org.apache.juli.AsyncFileHandler.level = WARNING
4error.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
4error.org.apache.juli.AsyncFileHandler.prefix = error.
4error.org.apache.juli.AsyncFileHandler.maxDays = 60
4error.org.apache.juli.AsyncFileHandler.encoding = UTF-8
# 特定包的日志级别
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = INFO
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].handlers = 2localhost.org.apache.juli.AsyncFileHandler
# 数据库连接池日志
org.apache.tomcat.jdbc.pool.level = WARNING
org.apache.tomcat.jdbc.pool.handlers = 4error.org.apache.juli.AsyncFileHandler
# 安全相关日志
org.apache.catalina.realm.level = INFO
org.apache.catalina.authenticator.level = INFO
2.2 应用特定日志配置
<!-- Context中的日志配置 -->
<Context path="/myapp" docBase="myapp.war">
<!-- 应用专用日志处理器 -->
<Logger className="org.apache.catalina.logger.FileLogger"
directory="logs"
prefix="myapp."
suffix=".log"
timestamp="true"
verbosity="2" />
</Context>
3. 访问日志配置
3.1 基础访问日志
<!-- server.xml - 访问日志配置 -->
<Host name="localhost" appBase="webapps">
<!-- 基础访问日志 -->
<Valve className="org.apache.catalina.valves.AccessLogValve"
directory="logs"
prefix="localhost_access_log"
suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
3.2 详细访问日志
<!-- 详细访问日志配置 -->
<Host name="localhost" appBase="webapps">
<Valve className="org.apache.catalina.valves.AccessLogValve"
directory="logs"
prefix="access_log"
suffix=".log"
pattern='%h %l %u %t "%r" %s %b %D "%{Referer}i" "%{User-Agent}i" %{X-Forwarded-For}i %{JSESSIONID}c'
rotatable="true"
maxDays="30"
encoding="UTF-8"
buffered="false"
resolveHosts="false"
requestAttributesEnabled="true" />
</Host>
3.3 JSON格式访问日志
<!-- JSON格式访问日志 -->
<Valve className="org.apache.catalina.valves.AccessLogValve"
directory="logs"
prefix="access_json"
suffix=".log"
pattern='{"timestamp":"%{yyyy-MM-dd HH:mm:ss}t","remote_addr":"%h","remote_user":"%u","request":"%r","status":%s,"bytes":%b,"response_time":%D,"referer":"%{Referer}i","user_agent":"%{User-Agent}i","forwarded_for":"%{X-Forwarded-For}i"}'
rotatable="true"
maxDays="15" />
4. 日志轮转管理
4.1 内置日志轮转
# 启用内置日志轮转
1catalina.org.apache.juli.AsyncFileHandler.rotatable = true
1catalina.org.apache.juli.AsyncFileHandler.maxDays = 30
1catalina.org.apache.juli.AsyncFileHandler.maxSize = 100MB
4.2 Logrotate配置
# /etc/logrotate.d/tomcat
/opt/tomcat9/logs/*.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
copytruncate
postrotate
systemctl reload tomcat > /dev/null 2>&1 || true
endscript
}
/opt/tomcat9/logs/catalina.out {
daily
missingok
rotate 7
compress
delaycompress
notifempty
copytruncate
size 100M
}
4.3 自定义日志轮转脚本
#!/bin/bash
# tomcat-log-rotate.sh
CATALINA_HOME="/opt/tomcat9"
LOG_DIR="$CATALINA_HOME/logs"
BACKUP_DIR="/var/backups/tomcat-logs"
DAYS_TO_KEEP=30
MAX_SIZE="100M"
# 创建备份目录
mkdir -p "$BACKUP_DIR"
rotate_log_file() {
local log_file="$1"
local max_size="$2"
if [ ! -f "$log_file" ]; then
return
fi
# 检查文件大小
local file_size=$(stat -f%z "$log_file" 2>/dev/null || stat -c%s "$log_file" 2>/dev/null)
local max_bytes=$(echo "$max_size" | sed 's/M/*1024*1024/g' | bc)
if [ "$file_size" -gt "$max_bytes" ]; then
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_file="$BACKUP_DIR/$(basename $log_file).$timestamp"
echo "轮转日志文件: $log_file"
# 复制并压缩
cp "$log_file" "$backup_file"
gzip "$backup_file"
# 清空原文件
> "$log_file"
echo "日志已轮转到: $backup_file.gz"
fi
}
# 轮转主要日志文件
rotate_log_file "$LOG_DIR/catalina.out" "$MAX_SIZE"
rotate_log_file "$LOG_DIR/catalina.$(date +%Y-%m-%d).log" "$MAX_SIZE"
# 轮转访问日志
for access_log in "$LOG_DIR"/localhost_access_log*.txt; do
[ -f "$access_log" ] && rotate_log_file "$access_log" "$MAX_SIZE"
done
# 清理过期备份
find "$BACKUP_DIR" -name "*.gz" -mtime +$DAYS_TO_KEEP -delete
# 重新加载Tomcat(如果需要)
if [ "$1" = "--reload" ]; then
systemctl reload tomcat
fi
echo "日志轮转完成"
5. 日志监控与分析
5.1 实时日志监控
#!/bin/bash
# log-monitor.sh
CATALINA_HOME="/opt/tomcat9"
LOG_DIR="$CATALINA_HOME/logs"
ALERT_EMAIL="admin@example.com"
# 监控错误日志
monitor_errors() {
echo "=== 错误日志监控 ==="
# 检查最近的错误
recent_errors=$(tail -1000 "$LOG_DIR/catalina.out" | grep -E "(ERROR|SEVERE|Exception)" | wc -l)
if [ "$recent_errors" -gt 10 ]; then
echo "警告: 发现 $recent_errors 个错误"
tail -100 "$LOG_DIR/catalina.out" | grep -E "(ERROR|SEVERE|Exception)" | \
tail -10 | mail -s "Tomcat错误告警" "$ALERT_EMAIL"
fi
# 检查OutOfMemoryError
if tail -1000 "$LOG_DIR/catalina.out" | grep -q "OutOfMemoryError"; then
echo "严重: 检测到内存溢出错误"
echo "内存溢出错误" | mail -s "Tomcat内存告警" "$ALERT_EMAIL"
fi
}
# 监控访问日志异常
monitor_access() {
echo "=== 访问日志监控 ==="
latest_access_log=$(ls -t "$LOG_DIR"/localhost_access_log*.txt | head -1)
if [ -f "$latest_access_log" ]; then
# 检查404错误
error_404=$(tail -1000 "$latest_access_log" | grep '" 404 ' | wc -l)
if [ "$error_404" -gt 50 ]; then
echo "警告: 404错误过多 ($error_404)"
fi
# 检查500错误
error_500=$(tail -1000 "$latest_access_log" | grep '" 5[0-9][0-9] ' | wc -l)
if [ "$error_500" -gt 10 ]; then
echo "警告: 服务器错误过多 ($error_500)"
fi
# 检查响应时间
slow_requests=$(tail -1000 "$latest_access_log" | awk '$NF > 5000 {count++} END {print count+0}')
if [ "$slow_requests" -gt 20 ]; then
echo "警告: 慢请求过多 ($slow_requests)"
fi
fi
}
# 日志文件大小监控
monitor_log_size() {
echo "=== 日志大小监控 ==="
for log_file in "$LOG_DIR"/*.log "$LOG_DIR"/catalina.out; do
if [ -f "$log_file" ]; then
size_mb=$(du -m "$log_file" | cut -f1)
if [ "$size_mb" -gt 500 ]; then
echo "警告: $log_file 大小过大 (${size_mb}MB)"
fi
fi
done
}
case "$1" in
"errors") monitor_errors ;;
"access") monitor_access ;;
"size") monitor_log_size ;;
"all")
monitor_errors
echo
monitor_access
echo
monitor_log_size
;;
*) echo "用法: $0 {errors|access|size|all}" ;;
esac
5.2 日志分析工具
#!/bin/bash
# log-analyzer.sh
LOG_DIR="/opt/tomcat9/logs"
analyze_access_log() {
local log_file="$1"
if [ ! -f "$log_file" ]; then
echo "日志文件不存在: $log_file"
return 1
fi
echo "=== 访问日志分析: $(basename $log_file) ==="
# 总请求数
total_requests=$(wc -l < "$log_file")
echo "总请求数: $total_requests"
# 状态码统计
echo -e "\n状态码分布:"
awk '{print $9}' "$log_file" | sort | uniq -c | sort -nr | head -10
# 访问最多的IP
echo -e "\n访问最多的IP:"
awk '{print $1}' "$log_file" | sort | uniq -c | sort -nr | head -10
# 访问最多的页面
echo -e "\n访问最多的页面:"
awk '{print $7}' "$log_file" | sort | uniq -c | sort -nr | head -10
# 响应时间分析
echo -e "\n响应时间分析:"
awk '{
if($NF ~ /^[0-9]+$/) {
total += $NF; count++;
if($NF > max) max = $NF;
if(min == 0 || $NF < min) min = $NF;
}
} END {
if(count > 0) {
printf "平均响应时间: %.2fms\n", total/count;
printf "最大响应时间: %dms\n", max;
printf "最小响应时间: %dms\n", min;
}
}' "$log_file"
# 小时访问量统计
echo -e "\n小时访问量分布:"
awk '{
match($4, /[0-9]+:[0-9]+/);
hour = substr($4, RSTART, 2);
count[hour]++;
} END {
for(h in count) printf "%02d:00 - %d\n", h, count[h];
}' "$log_file" | sort
}
analyze_error_log() {
local log_file="$1"
echo "=== 错误日志分析: $(basename $log_file) ==="
# 错误级别统计
echo "错误级别统计:"
grep -E "(SEVERE|WARNING|INFO)" "$log_file" | \
sed 's/.*\(SEVERE\|WARNING\|INFO\).*/\1/' | sort | uniq -c
# 常见异常统计
echo -e "\n常见异常:"
grep -o "[A-Za-z]*Exception\|[A-Za-z]*Error" "$log_file" | sort | uniq -c | sort -nr | head -10
# 最近的严重错误
echo -e "\n最近的严重错误:"
grep "SEVERE" "$log_file" | tail -5
}
generate_daily_report() {
local date="${1:-$(date +%Y-%m-%d)}"
local report_file="/tmp/tomcat_report_$date.txt"
echo "生成日期报告: $date"
{
echo "Tomcat日志分析报告 - $date"
echo "=============================="
# 分析访问日志
for access_log in "$LOG_DIR"/localhost_access_log*.txt; do
if [[ $(basename "$access_log") =~ $date ]]; then
analyze_access_log "$access_log"
echo
fi
done
# 分析错误日志
for error_log in "$LOG_DIR"/catalina.*.log; do
if [[ $(basename "$error_log") =~ $date ]]; then
analyze_error_log "$error_log"
echo
fi
done
} > "$report_file"
echo "报告已生成: $report_file"
}
case "$1" in
"access") analyze_access_log "$2" ;;
"error") analyze_error_log "$2" ;;
"report") generate_daily_report "$2" ;;
*)
echo "用法: $0 {access|error|report} [文件/日期]"
echo " access <日志文件> - 分析访问日志"
echo " error <日志文件> - 分析错误日志"
echo " report [日期] - 生成日期报告"
;;
esac
6. 结构化日志配置
6.1 JSON日志配置
// JSONFormatter.java
package com.example.logging;
import java.util.logging.Formatter;
import java.util.logging.LogRecord;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class JSONFormatter extends Formatter {
private static final DateTimeFormatter DATE_FORMAT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
.withZone(ZoneId.systemDefault());
@Override
public String format(LogRecord record) {
StringBuilder json = new StringBuilder();
json.append("{");
json.append("\"timestamp\":\"").append(DATE_FORMAT.format(Instant.ofEpochMilli(record.getMillis()))).append("\",");
json.append("\"level\":\"").append(record.getLevel()).append("\",");
json.append("\"logger\":\"").append(record.getLoggerName()).append("\",");
json.append("\"message\":\"").append(escapeJson(formatMessage(record))).append("\",");
json.append("\"thread\":\"").append(Thread.currentThread().getName()).append("\"");
if (record.getThrown() != null) {
json.append(",\"exception\":\"").append(escapeJson(record.getThrown().toString())).append("\"");
}
json.append("}\n");
return json.toString();
}
private String escapeJson(String input) {
if (input == null) return "";
return input.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
}
配置使用JSON格式器:
# 使用JSON格式器
1catalina.org.apache.juli.AsyncFileHandler.formatter = com.example.logging.JSONFormatter
2localhost.org.apache.juli.AsyncFileHandler.formatter = com.example.logging.JSONFormatter
6.2 MDC支持
// MDCFilter.java
package com.example.logging;
import org.slf4j.MDC;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
public class MDCFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
try {
// 添加追踪ID
String traceId = UUID.randomUUID().toString();
MDC.put("traceId", traceId);
// 添加请求信息
MDC.put("requestURI", httpRequest.getRequestURI());
MDC.put("remoteAddr", httpRequest.getRemoteAddr());
MDC.put("userAgent", httpRequest.getHeader("User-Agent"));
// 添加会话ID
if (httpRequest.getSession(false) != null) {
MDC.put("sessionId", httpRequest.getSession().getId());
}
chain.doFilter(request, response);
} finally {
// 清理MDC
MDC.clear();
}
}
}
7. 日志最佳实践
7.1 生产环境日志配置
# 生产环境推荐配置
handlers = 1catalina.org.apache.juli.AsyncFileHandler, 2localhost.org.apache.juli.AsyncFileHandler, 3error.org.apache.juli.AsyncFileHandler
.handlers = 1catalina.org.apache.juli.AsyncFileHandler
.level = INFO
# 主日志 - 仅记录重要信息
1catalina.org.apache.juli.AsyncFileHandler.level = INFO
1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina.
1catalina.org.apache.juli.AsyncFileHandler.maxDays = 7
1catalina.org.apache.juli.AsyncFileHandler.bufferSize = 32768
# 错误日志 - 记录警告和错误
3error.org.apache.juli.AsyncFileHandler.level = WARNING
3error.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
3error.org.apache.juli.AsyncFileHandler.prefix = error.
3error.org.apache.juli.AsyncFileHandler.maxDays = 30
# 禁用过于详细的日志
org.apache.coyote.level = WARNING
org.apache.tomcat.util.net.level = WARNING
org.apache.catalina.session.level = WARNING
7.2 日志安全配置
#!/bin/bash
# secure-logs.sh
CATALINA_HOME="/opt/tomcat9"
LOG_DIR="$CATALINA_HOME/logs"
# 设置日志文件权限
chmod 640 "$LOG_DIR"/*.log
chmod 640 "$LOG_DIR"/catalina.out
chown tomcat:tomcat "$LOG_DIR"/*.log
chown tomcat:tomcat "$LOG_DIR"/catalina.out
# 设置日志目录权限
chmod 750 "$LOG_DIR"
chown tomcat:tomcat "$LOG_DIR"
echo "日志文件权限已设置"
小结
通过本文学习,你应该掌握:
- Tomcat日志系统架构和组件
- 日志级别和处理器配置
- 访问日志的详细配置方法
- 日志轮转和管理策略
- 日志监控和分析技术
- 结构化日志和MDC使用
- 日志安全和最佳实践
下一篇文章将介绍Tomcat监控与诊断技术。