第十四章:实现 Log 模块

功能目标与实现内容

本章旨在为 MiniTomcat 实现一个基础的 Log 模块,主要目标包括:

  • 日志记录:支持记录请求日志、错误日志和系统日志。
  • 级别管理:支持不同的日志级别(INFO、DEBUG、ERROR 等),以便控制日志的详细程度。
  • 实现方式:设计一个简单的 Logger 接口及实现类,提供不同级别的日志输出,并支持配置输出格式和文件路径。

14.1 日志记录的重要性

在 Web 应用开发中,日志记录是至关重要的环节。日志帮助我们跟踪系统的运行状态、诊断问题,并提供可用的监控信息。常见的日志类型包括:

  • 请求日志:记录每个 HTTP 请求的相关信息。
  • 错误日志:记录程序异常和错误信息。
  • 系统日志:记录系统级别的信息,如服务启动、停止等事件。

为了更高效地记录日志,我们需要将日志分为不同的级别,并根据日志级别来控制日志内容的输出。


14.2 日志级别的设计

常见的日志级别如下,我们可以通过控制日志级别来决定输出多少日志信息,避免在生产环境中输出过多的调试信息:

  • DEBUG:最详细的日志,用于调试阶段,记录系统的详细信息。
  • INFO:常规信息,记录系统的正常操作,如请求处理过程等。
  • WARN:警告信息,用于记录可能出现问题的地方,但不一定会导致错误。
  • ERROR:错误信息,用于记录异常或错误,系统无法继续运行的情况。
  • FATAL:严重错误,通常导致系统终止。

14.3 实现 Logger 类

实现步骤

1. 定义日志级别枚举

首先定义日志级别的枚举类:

public enum LogLevel {
    DEBUG, INFO, WARN, ERROR, FATAL
}

2. 创建日志记录器接口

定义 Logger 接口,规范日志记录的行为:

package com.daicy.minitomcat.log;

public interface Logger {

    void log(LogLevel level, String message);

    void log(LogLevel level, String message, Throwable throwable);

    // debug 级别日志
    void debug(String message);

    // info 级别日志
    void info(String message);

    // warn 级别日志
    void warn(String message);

    // error 级别日志
    void error(String message);
}

3. 创建抽象日志类

创建 AbstractLogger 抽象类,实现公共逻辑(如级别判断、时间格式化):

package com.daicy.minitomcat.log;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public abstract class AbstractLogger implements Logger {

    final LogLevel minLogLevel;

    protected AbstractLogger(LogLevel minLogLevel) {
        this.minLogLevel = minLogLevel;
    }

    private String throwableToString(Throwable throwable) {
        StringBuilder sb = new StringBuilder();
        for (StackTraceElement element : throwable.getStackTrace()) {
            sb.append("\tat ").append(element).append("\n");
        }
        return sb.toString();
    }

    String getCurrentTime() {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        return LocalDateTime.now().format(formatter);
    }

    @Override
    public void log(LogLevel level, String message, Throwable throwable) {
        if (level.ordinal() >= minLogLevel.ordinal()) {
            String logMessage = message + "\n" + throwableToString(throwable);
            log(level, logMessage);
        }
    }

    // debug 级别日志
    @Override
    public void debug(String message) {
        log(LogLevel.DEBUG, message);
    }

    // info 级别日志
    @Override
    public void info(String message) {
        log(LogLevel.INFO, message);
    }

    // warn 级别日志
    @Override
    public void warn(String message) {
        log(LogLevel.WARN, message);
    }

    // error 级别日志
    @Override
    public void error(String message) {
        log(LogLevel.ERROR, message);
    }
}
注意AbstractLogger 未直接实现 log(LogLevel level, String message) 方法,因为该类是抽象类,具体实现将由子类(如 ConsoleLoggerFileLogger)完成。

4. 实现基础日志类(控制台)

创建 ConsoleLogger,将日志输出到控制台:

package com.daicy.minitomcat.log;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class ConsoleLogger extends AbstractLogger {

    public ConsoleLogger(LogLevel minLogLevel) {
        super(minLogLevel);
    }

    @Override
    public void log(LogLevel level, String message) {
        if (level.ordinal() >= minLogLevel.ordinal()) {
            System.out.println(formatLog(level, message));
        }
    }

    @Override
    public void log(LogLevel level, String message, Throwable throwable) {
        if (level.ordinal() >= minLogLevel.ordinal()) {
            System.out.println(formatLog(level, message));
            throwable.printStackTrace(System.out);
        }
    }

    private String formatLog(LogLevel level, String message) {
        String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return String.format("[%s] [%s] %s", timestamp, level, message);
    }
}

5. 增加文件日志支持

扩展 FileLogger,将日志保存到文件:

package com.daicy.minitomcat.log;

import java.io.FileWriter;
import java.io.IOException;

public class FileLogger extends AbstractLogger {
    private final String logFilePath;

    public FileLogger(LogLevel minLogLevel, String logFilePath) {
        super(minLogLevel);
        this.logFilePath = logFilePath;
    }

    @Override
    public void log(LogLevel level, String message) {
        if (level.ordinal() >= minLogLevel.ordinal()) {
            String logMessage = String.format("[%s] [%s] %s", getCurrentTime(), level, message);
            // 输出到文件
            try (FileWriter writer = new FileWriter(logFilePath, true)) {
                writer.write(logMessage + "\n");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

6. 创建日志管理器

日志管理器 LogManager 用于统一管理日志记录器,支持多日志源输出:

package com.daicy.minitomcat.log;

import java.util.ArrayList;
import java.util.List;

public class LogManager {
    private static final List<Logger> loggers = new ArrayList<>();

    public static void addLogger(Logger logger) {
        loggers.add(logger);
    }

    public static void log(LogLevel level, String message) {
        for (Logger logger : loggers) {
            logger.log(level, message);
        }
    }

    public static void log(LogLevel level, String message, Throwable throwable) {
        for (Logger logger : loggers) {
            logger.log(level, message, throwable);
        }
    }

    // debug 级别日志
    public static void debug(String message) {
        log(LogLevel.DEBUG, message);
    }

    // info 级别日志
    public static void info(String message) {
        log(LogLevel.INFO, message);
    }

    // warn 级别日志
    public static void warn(String message) {
        log(LogLevel.WARN, message);
    }

    // error 级别日志
    public static void error(String message) {
        log(LogLevel.ERROR, message);
    }
}

7. 集成日志模块

在 MiniTomcat 启动时,添加统一的日志调用配置:

LogManager.addLogger(new ConsoleLogger(LogLevel.INFO));
LogManager.addLogger(new FileLogger(LogLevel.DEBUG, "mini-tomcat.log"));

在需要记录日志的地方调用:

LogManager.log(LogLevel.INFO, "MiniTomcat started");
LogManager.log(LogLevel.ERROR, "Error handling request", exception);

测试日志模块

  1. 启动 MiniTomcat,观察控制台是否输出正确的日志。
  2. 查看日志文件内容,确保日志条目正确写入。
  3. 修改日志级别,验证不同级别的日志过滤效果。

优化与扩展

  • 异步日志:使用线程池异步写入日志,提升性能。
  • 日志分割:按日期或文件大小分割日志文件。
  • 可配置性:从外部配置文件读取日志设置,例如日志级别、输出路径等。

通过以上步骤,我们成功为 MiniTomcat 构建了一个灵活、高效的日志模块。


14.4 配置和使用 Logger

在实际使用中,我们可以通过 LogManager 记录 Web 容器中的各种日志信息。

14.4.1 配置和使用示例

以下是一个 Filter 示例,用于记录请求的处理时间:

package com.daicy.minitomcat;

import com.daicy.minitomcat.log.LogManager;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class LoggingFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化操作,如果有需要可以在这里读取配置参数等
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        long startTime = System.currentTimeMillis();
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        LogManager.info("Request started at: " + startTime + " for path: " + request.getRequestURI());

        filterChain.doFilter(servletRequest, servletResponse);

        long endTime = System.currentTimeMillis();
        LogManager.info("Request completed at: " + endTime + " for path: " + request.getRequestURI() + " Took: " + (endTime - startTime) + "ms");
    }

    @Override
    public void destroy() {
        // 清理资源操作
    }
}

在这个示例中,我们使用了 LogManager 进行日志记录。假设控制台日志级别设置为 INFO,这意味着 DEBUG 级别的日志将不会输出,而 INFOWARNERROR 级别的日志会被输出。

输出结果(控制台和 mini-tomcat.log 文件中)如下:

[2024-11-23 15:39:00] [INFO] Servlet context initialized.
[2024-11-23 15:39:00] [INFO] HelloServlet initialized.
[2024-11-23 15:39:00] [INFO] HTTP Connector is running on port 8080
[2024-11-23 15:39:07] [INFO] Accepted connection from /0:0:0:0:0:0:0:1
[2024-11-23 15:39:07] [INFO] Accepted connection from /0:0:0:0:0:0:0:1
[2024-11-23 15:39:07] [INFO] Session created with ID: afeb595f-354d-4b6a-a136-f11ba15f4bb2
[2024-11-23 15:39:07] [INFO] LogValve: Logging request /hello
[2024-11-23 15:39:07] [INFO] Request started at: 1732347547912 for path: /hello
[2024-11-23 15:39:07] [INFO] Request completed at: 1732347547912 for path: /hello Took: 0ms
[2024-11-23 15:39:40] [INFO] Server stopping...
[2024-11-23 15:39:40] [INFO] HelloServlet destroyed.
[2024-11-23 15:39:40] [INFO] Servlet context destroyed.

14.5 日志管理和扩展

14.5.1 日志级别管理

在生产环境中,我们通常会将日志级别设置为 INFO 或更高的级别,这样可以避免过多的调试信息输出。而在开发和调试过程中,DEBUG 级别的日志有助于我们进行问题排查。

14.5.2 日志文件管理

为了防止日志文件过大,我们可以定期轮换日志文件。例如,使用时间戳或文件大小来分割日志文件。日志文件的管理可以通过日志框架(如 Log4j 或 SLF4J)来实现,本示例中为简化实现未包含此功能。

14.5.3 日志的多线程安全

如果系统是多线程的,日志输出可能会受到线程竞争的影响。可以通过同步方法或使用线程安全的日志框架来确保日志输出的正确性。本示例中的 FileWriter 在多线程环境下可能需要额外的同步处理。


14.6 学习收获

通过实现日志模块,我们学习了以下内容:

  1. 日志级别控制:掌握了日志级别控制的原理,理解了如何根据不同的日志级别输出不同的日志信息。
  2. 日志输出到文件和控制台:实现了日志的双重输出,既能在控制台显示,也能记录到日志文件中,方便后期分析。
  3. 日志模块的可扩展性:通过 Logger 类的设计,我们可以灵活地扩展日志模块,支持更多功能,如日志轮转、异步日志等。

通过日志模块的实现,我们为 Web 容器的调试、监控和运维提供了基础支持。

说明:本文代码示例使用了 java.time 包,适用于 Java 8 及以上版本。在生产环境中,建议直接使用成熟的日志框架(如 Logback、Log4j2)以获得更好的性能和功能支持。