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 &quot;%r&quot; %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 "日志文件权限已设置"

小结

通过本文学习,你应该掌握:

  1. Tomcat日志系统架构和组件
  2. 日志级别和处理器配置
  3. 访问日志的详细配置方法
  4. 日志轮转和管理策略
  5. 日志监控和分析技术
  6. 结构化日志和MDC使用
  7. 日志安全和最佳实践

下一篇文章将介绍Tomcat监控与诊断技术。

powered by Gitbook© 2025 编外计划 | 最后修改: 2025-08-29 15:40:15

results matching ""

    No results matching ""