XLucas Logger 日志组件说明书

一、概述

XLucas Logger 是一款轻量级 PHP 日志记录组件,提供灵活的日志管理功能,包括多级别日志记录、自动文件轮转、过期日志清理、配置自定义等特性,适用于各类 PHP 应用的日志跟踪与管理。

二、功能特性

  1. 多级别日志记录
    支持 5 个日志等级(从低到高):

    • DEBUG(调试):用于开发阶段的详细调试信息
    • INFO(信息):记录程序运行的常规状态信息
    • SUCCESS(成功):标记操作成功的事件(如"数据保存成功")
    • WARNING(警告):记录非致命性异常(如"参数格式不规范")
    • ERROR(错误):记录致命性错误(如"数据库连接失败")
  2. 自动日志文件轮转
    当当前日志文件大小超过配置的MaxFileSize时,自动将旧日志文件重命名为带时间戳的归档文件(如app_2024-05-20_15-30-45.log),并创建新文件继续记录,避免单文件过大。

  3. 自动清理过期日志
    当日志文件总数超过MaxLogFiles配置时,自动删除最早创建的日志文件,确保磁盘空间不被过度占用。

  4. 灵活的配置管理
    支持通过配置文件或代码动态修改日志参数(如存储路径、日志等级阈值、文件大小限制等)。

  5. 日志操作工具
    提供日志查看(读取最后N行或全部内容)、日志清除(删除所有日志文件)、日志信息统计(文件数量、总大小等)功能。

  6. 控制台输出支持
    可配置是否同时将日志输出到控制台,方便开发环境调试。

三、配置说明

日志组件的行为通过配置项控制,支持默认配置、配置文件自定义和代码动态修改三种方式。

1. 配置项说明

配置项 类型 说明 默认值
LogPath string 日志文件存储目录 优先使用storage_path('logs')(ThinkPHP 环境),否则为../runtime/logs
LogFileName string 主日志文件名(未轮转的当前日志) app.log
MaxFileSize int 单个日志文件最大大小(字节) 10MB(1010241024)
MaxLogFiles int 最多保留的日志文件总数(含当前日志和归档日志) 10
MinLevel int 最小记录等级(低于该等级的日志不记录):0=DEBUG,1=INFO,2=SUCCESS,3=WARNING,4=ERROR 0(记录所有等级)
DateFormat string 日志中的时间格式(遵循 PHP date() 函数格式) Y-m-d H:i:s
EnableConsole bool 是否同时将日志输出到控制台 false

2. 配置方式

(1)配置文件方式

在组件目录下创建LogConfig.php(默认已提供),返回配置数组即可生效。示例:

<?php
return [
    'LogPath' => __DIR__ . '/../../../runtime/logs',
    'LogFileName' => 'XLucas_0554H.log',
    'MaxFileSize' => 2 * 1024 * 1024, // 2MB
    'MaxLogFiles' => 30,
    'MinLevel' => 1, // 不记录DEBUG
    'DateFormat' => 'Y-m-d H:i:s',
    'EnableConsole' => true,
];

(2)代码动态修改

通过SetConfig()方法动态修改配置,示例:

use XLucas\Logger\Log;

// 动态设置最大文件大小为5MB,启用控制台输出
Log::SetConfig([
    'MaxFileSize' => 5 * 1024 * 1024,
    'EnableConsole' => true
]);

四、使用方法

1. 引入组件

确保命名空间正确,通过use语句引入:

use XLucas\Logger\Log;

2. 记录日志

通过静态方法直接记录不同等级的日志,参数为日志消息字符串:

// 记录调试日志
Log::Debug('用户ID=123的请求参数:' . json_encode($_POST));

// 记录信息日志
Log::Info('系统启动完成,环境:production');

// 记录成功日志
Log::Success('订单ID=456支付成功');

// 记录警告日志
Log::Warning('用户ID=789的手机号格式不正确:12345');

// 记录错误日志
Log::Error('数据库连接失败:' . mysqli_error($conn));

3. 查看日志

通过Read()方法查看日志内容:

// 查看全部日志
$allLogs = Log::Read();
echo $allLogs;

// 查看最后100行日志
$last100Lines = Log::Read(100);
echo $last100Lines;

4. 清除日志

通过Clear()方法删除所有日志文件(含当前日志和归档日志):

$isSuccess = Log::Clear();
if ($isSuccess) {
    echo "日志清除成功";
} else {
    echo "日志清除失败";
}

5. 获取日志信息

通过GetInfo()方法获取日志统计信息(文件数量、大小、路径等):

$logInfo = Log::GetInfo();
print_r($logInfo);

返回结果示例:

[
    'LogPath' => '/path/to/logs',
    'CurrentLogFile' => '/path/to/logs/app.log',
    'TotalFiles' => 5,
    'TotalSize' => 2048000,
    'TotalSizeFormatted' => '2 MB',
    'MaxFileSize' => 10485760,
    'MaxFileSizeFormatted' => '10 MB',
    'MaxLogFiles' => 10,
    'MinLevel' => 'DEBUG',
    'Files' => [
        [
            'Name' => 'app.log',
            'Path' => '/path/to/logs/app.log',
            'Size' => 512000,
            'SizeFormatted' => '500 KB',
            'ModifiedTime' => '2024-05-20 16:30:00',
            'IsCurrent' => true
        ],
        // ... 其他日志文件信息
    ]
]

6. 获取当前配置

通过GetConfig()方法获取当前生效的配置:

$config = Log::GetConfig();
print_r($config);

五、日志文件管理机制

  1. 文件轮转规则
    当当前日志文件(如app.log)大小达到MaxFileSize时,自动重命名为app_YYYY-MM-DD_HH-ii-ss.log(带时间戳),并创建新的app.log继续记录。

  2. 过期清理规则
    每次写入日志时检查文件总数,若超过MaxLogFiles,则按修改时间排序,删除最早的文件,保留最新的N个文件(N=MaxLogFiles)。

  3. 日志格式
    每条日志格式为:[时间] [等级] 消息,例如:

    [2024-05-20 15:30:45] [INFO] 系统启动完成
    [2024-05-20 15:31:00] [ERROR] 数据库连接失败

六、注意事项

  1. 目录权限
    确保LogPath配置的目录具有写入权限(建议权限0777),否则日志无法写入。

  2. 配置加载顺序
    优先加载LogConfig.php,若文件不存在则使用默认配置;通过SetConfig()设置的配置会覆盖上述两种方式。

  3. 日志等级控制
    生产环境建议将MinLevel设置为INFO或更高,避免DEBUG日志占用过多磁盘空间。

  4. 性能影响
    日志写入涉及文件操作,高频日志记录可能影响性能,建议核心业务按需记录关键日志。

七、版本信息

  • 版本:1.0.0
  • 作者:XLucas

八、程序代码

配置文件

/**
 * 日志配置文件
 * 
 * 配置说明:
 * - LogPath: 日志文件存储目录
 * - LogFileName: 日志文件名
 * - MaxFileSize: 单个日志文件最大大小(字节)
 * - MaxLogFiles: 最多保留的日志文件数量
 * - MinLevel: 最小记录等级(0=DEBUG, 1=INFO, 2=SUCCESS, 3=WARNING, 4=ERROR)
 * - DateFormat: 日期时间格式
 * - EnableConsole: 是否启用控制台输出
 */
return [
    // 日志文件存储目录
    // 注意:如果在 ThinkPHP 中使用,可以改为 storage_path('logs')
    'LogPath' => __DIR__ . '/../../../runtime/logs',

    // 日志文件名
    'LogFileName' => 'XLucas_0554H.log',

    // 单个日志文件最大大小(字节)
    // 10MB = 10 * 1024 * 1024
    'MaxFileSize' => 2 * 1024 * 1024 ,

    // 最多保留的日志文件数量
    'MaxLogFiles' => 30,

    // 最小记录等级
    // 0 = DEBUG(记录所有)
    // 1 = INFO(不记录DEBUG)
    // 2 = SUCCESS(不记录DEBUG、INFO)
    // 3 = WARNING(只记录WARNING、ERROR)
    // 4 = ERROR(只记录ERROR)
    'MinLevel' => 0,

    // 日期时间格式
    'DateFormat' => 'Y-m-d H:i:s',

    // 是否启用控制台输出
    'EnableConsole' => false,
];

主程序

namespace XLucas\Logger;

/**
 * 日志记录扩展程序
 * 
 * 功能特性:
 * - 支持5个日志等级(Debug、Info、Success、Warning、Error)
 * - 自动轮转日志文件(超过最大大小时重命名旧日志)
 * - 自动清理过期日志(超过最大数量时删除最早的日志)
 * - 可配置的日志等级、最大大小、最大数量
 * - 支持查看和清除日志
 * 
 * @author XLucas
 * @version 1.0.0
 */
class Log
{
    // 日志等级常量
    const LEVEL_DEBUG = 0;
    const LEVEL_INFO = 1;
    const LEVEL_SUCCESS = 2;
    const LEVEL_WARNING = 3;
    const LEVEL_ERROR = 4;

    // 日志等级名称
    private static $LevelNames = [
        self::LEVEL_DEBUG => 'DEBUG',
        self::LEVEL_INFO => 'INFO',
        self::LEVEL_SUCCESS => 'SUCCESS',
        self::LEVEL_WARNING => 'WARNING',
        self::LEVEL_ERROR => 'ERROR',
    ];

    // 日志等级颜色(用于控制台输出)
    private static $LevelColors = [
        self::LEVEL_DEBUG => '[DEBUG]',
        self::LEVEL_INFO => '[INFO]',
        self::LEVEL_SUCCESS => '[SUCCESS]',
        self::LEVEL_WARNING => '[WARNING]',
        self::LEVEL_ERROR => '[ERROR]',
    ];

    // 配置信息
    private static $Config = null;

    // 当前日志文件路径
    private static $CurrentLogFile = null;

    /**
     * 初始化配置
     */
    private static function InitConfig()
    {
        if (self::$Config !== null) {
            return;
        }

        // 加载配置
        $ConfigFile = __DIR__ . '/LogConfig.php';
        if (file_exists($ConfigFile)) {
            self::$Config = require $ConfigFile;
        } else {
            // 默认配置
            // 尝试使用 ThinkPHP 的 storage_path(),如果不存在则使用相对路径
            $LogPath = function_exists('storage_path') 
                ? storage_path('logs') 
                : __DIR__ . '/../../runtime/logs';

            self::$Config = [
                'LogPath' => $LogPath,
                'LogFileName' => 'app.log',
                'MaxFileSize' => 10 * 1024 * 1024, // 10MB
                'MaxLogFiles' => 10,
                'MinLevel' => self::LEVEL_DEBUG,
                'DateFormat' => 'Y-m-d H:i:s',
                'EnableConsole' => false,
            ];
        }

        // 确保日志目录存在
        if (!is_dir(self::$Config['LogPath'])) {
            @mkdir(self::$Config['LogPath'], 0777, true);
        }
    }

    /**
     * 获取当前日志文件路径
     */
    private static function GetLogFilePath()
    {
        if (self::$CurrentLogFile === null) {
            self::InitConfig();
            self::$CurrentLogFile = self::$Config['LogPath'] . DIRECTORY_SEPARATOR . self::$Config['LogFileName'];
        }
        return self::$CurrentLogFile;
    }

    /**
     * 检查并处理日志文件轮转
     */
    private static function CheckAndRotateLog()
    {
        $LogFile = self::GetLogFilePath();

        // 检查文件是否存在且超过最大大小
        if (file_exists($LogFile) && filesize($LogFile) >= self::$Config['MaxFileSize']) {
            self::RotateLogFile($LogFile);
        }

        // 检查并清理过期日志(每次都检查,确保数量不超过限制)
        self::CleanOldLogs();
    }

    /**
     * 轮转日志文件(重命名旧日志,创建新日志)
     */
    private static function RotateLogFile($LogFile)
    {
        $PathInfo = pathinfo($LogFile);
        $Directory = $PathInfo['dirname'];
        $FileName = $PathInfo['filename'];
        $Extension = isset($PathInfo['extension']) ? '.' . $PathInfo['extension'] : '';

        // 生成新的文件名(带时间戳)
        $Timestamp = date('Y-m-d_H-i-s');
        $NewFileName = $FileName . '_' . $Timestamp . $Extension;
        $NewFilePath = $Directory . DIRECTORY_SEPARATOR . $NewFileName;

        // 重命名旧日志文件
        if (file_exists($LogFile)) {
            @rename($LogFile, $NewFilePath);
        }
    }

    /**
     * 清理过期的日志文件
     */
    private static function CleanOldLogs()
    {
        $LogPath = self::$Config['LogPath'];
        $LogFileName = self::$Config['LogFileName'];
        $MaxLogFiles = self::$Config['MaxLogFiles'];

        // 检查目录是否存在
        if (!is_dir($LogPath)) {
            return;
        }

        // 获取所有日志文件
        $Files = [];
        $DirContents = @scandir($LogPath);

        if ($DirContents === false) {
            return;
        }

        // 获取日志文件的基础名称(不包括扩展名)
        $PathInfo = pathinfo($LogFileName);
        $BaseFileName = $PathInfo['filename'];
        $Extension = isset($PathInfo['extension']) ? '.' . $PathInfo['extension'] : '';

        foreach ($DirContents as $File) {
            if ($File === '.' || $File === '..') {
                continue;
            }

            $FilePath = $LogPath . DIRECTORY_SEPARATOR . $File;
            if (is_file($FilePath)) {
                // 匹配日志文件(当前日志和归档日志)
                // 当前日志: test.log
                // 归档日志: test_YYYY-MM-DD_HH-ii-ss.log
                if ($File === $LogFileName || strpos($File, $BaseFileName . '_') === 0 && substr($File, -strlen($Extension)) === $Extension) {
                    $Files[$FilePath] = filemtime($FilePath);
                }
            }
        }

        // 如果日志文件数量超过最大数量,删除最早的
        if (count($Files) > $MaxLogFiles) {
            // 按修改时间排序(最早的在前)
            asort($Files);

            // 计算需要删除的文件数量
            $DeleteCount = count($Files) - $MaxLogFiles;

            // 删除最早的日志文件
            $Index = 0;
            foreach ($Files as $FilePath => $Time) {
                if ($Index >= $DeleteCount) {
                    break;
                }

                // 尝试删除文件
                if (@unlink($FilePath)) {
                    $Index++;
                }
            }
        }
    }

    /**
     * 记录日志
     */
    private static function WriteLog($Level, $Message)
    {
        self::InitConfig();

        // 检查日志等级是否满足最小等级要求
        if ($Level < self::$Config['MinLevel']) {
            return;
        }

        // 确保日志目录存在
        if (!is_dir(self::$Config['LogPath'])) {
            @mkdir(self::$Config['LogPath'], 0777, true);
        }

        // 检查并处理日志文件轮转
        self::CheckAndRotateLog();

        // 获取日志文件路径
        $LogFile = self::GetLogFilePath();

        // 格式化日志消息
        $Timestamp = date(self::$Config['DateFormat']);
        $LevelName = self::$LevelNames[$Level];
        $FormattedMessage = "[{$Timestamp}] [{$LevelName}] {$Message}" . PHP_EOL;

        // 写入日志文件
        $Handle = @fopen($LogFile, 'a');
        if ($Handle) {
            fwrite($Handle, $FormattedMessage);
            fclose($Handle);
        }

        // 如果启用控制台输出,输出到控制台
        if (self::$Config['EnableConsole']) {
            echo self::$LevelColors[$Level] . ' ' . $FormattedMessage;
        }
    }

    /**
     * 记录调试日志
     * 
     * @param string $Message 日志消息
     */
    public static function Debug($Message)
    {
        self::WriteLog(self::LEVEL_DEBUG, $Message);
    }

    /**
     * 记录信息日志
     * 
     * @param string $Message 日志消息
     */
    public static function Info($Message)
    {
        self::WriteLog(self::LEVEL_INFO, $Message);
    }

    /**
     * 记录成功日志
     * 
     * @param string $Message 日志消息
     */
    public static function Success($Message)
    {
        self::WriteLog(self::LEVEL_SUCCESS, $Message);
    }

    /**
     * 记录警告日志
     * 
     * @param string $Message 日志消息
     */
    public static function Warning($Message)
    {
        self::WriteLog(self::LEVEL_WARNING, $Message);
    }

    /**
     * 记录错误日志
     * 
     * @param string $Message 日志消息
     */
    public static function Error($Message)
    {
        self::WriteLog(self::LEVEL_ERROR, $Message);
    }

    /**
     * 清除所有日志文件
     * 
     * @return bool 是否清除成功
     */
    public static function Clear()
    {
        self::InitConfig();

        $LogPath = self::$Config['LogPath'];
        $LogFileName = self::$Config['LogFileName'];

        $DeletedCount = 0;
        if (is_dir($LogPath)) {
            $DirContents = @scandir($LogPath);
            if ($DirContents === false) {
                return false;
            }

            // 获取日志文件的基础名称(不包括扩展名)
            $PathInfo = pathinfo($LogFileName);
            $BaseFileName = $PathInfo['filename'];
            $Extension = isset($PathInfo['extension']) ? '.' . $PathInfo['extension'] : '';

            foreach ($DirContents as $File) {
                if ($File === '.' || $File === '..') {
                    continue;
                }

                $FilePath = $LogPath . DIRECTORY_SEPARATOR . $File;
                if (is_file($FilePath)) {
                    // 匹配日志文件(当前日志和归档日志)
                    if ($File === $LogFileName || strpos($File, $BaseFileName . '_') === 0 && substr($File, -strlen($Extension)) === $Extension) {
                        if (@unlink($FilePath)) {
                            $DeletedCount++;
                        }
                    }
                }
            }
        }

        return $DeletedCount > 0;
    }

    /**
     * 查看日志内容
     * 
     * @param int $Lines 查看最后N行(0表示查看全部)
     * @return string 日志内容
     */
    public static function Read($Lines = 0)
    {
        self::InitConfig();

        $LogFile = self::GetLogFilePath();

        if (!file_exists($LogFile)) {
            return '日志文件不存在';
        }

        $Content = file_get_contents($LogFile);

        // 如果指定了行数,只返回最后N行
        if ($Lines > 0) {
            $LogLines = explode(PHP_EOL, trim($Content));
            $LogLines = array_slice($LogLines, -$Lines);
            $Content = implode(PHP_EOL, $LogLines);
        }

        return $Content;
    }

    /**
     * 获取日志文件信息
     * 
     * @return array 日志文件信息
     */
    public static function GetInfo()
    {
        self::InitConfig();

        $LogPath = self::$Config['LogPath'];
        $LogFileName = self::$Config['LogFileName'];
        $CurrentLogFile = self::GetLogFilePath();

        $Files = [];
        $TotalSize = 0;

        if (is_dir($LogPath)) {
            $DirContents = @scandir($LogPath);
            if ($DirContents === false) {
                return [
                    'LogPath' => $LogPath,
                    'CurrentLogFile' => $CurrentLogFile,
                    'TotalFiles' => 0,
                    'TotalSize' => 0,
                    'TotalSizeFormatted' => '0 B',
                    'MaxFileSize' => self::$Config['MaxFileSize'],
                    'MaxFileSizeFormatted' => self::FormatBytes(self::$Config['MaxFileSize']),
                    'MaxLogFiles' => self::$Config['MaxLogFiles'],
                    'MinLevel' => self::$LevelNames[self::$Config['MinLevel']],
                    'Files' => [],
                ];
            }

            // 获取日志文件的基础名称(不包括扩展名)
            $PathInfo = pathinfo($LogFileName);
            $BaseFileName = $PathInfo['filename'];
            $Extension = isset($PathInfo['extension']) ? '.' . $PathInfo['extension'] : '';

            foreach ($DirContents as $File) {
                if ($File === '.' || $File === '..') {
                    continue;
                }

                $FilePath = $LogPath . DIRECTORY_SEPARATOR . $File;
                if (is_file($FilePath)) {
                    // 匹配日志文件(当前日志和归档日志)
                    // 当前日志: test.log
                    // 归档日志: test_YYYY-MM-DD_HH-ii-ss.log
                    if ($File === $LogFileName || strpos($File, $BaseFileName . '_') === 0 && substr($File, -strlen($Extension)) === $Extension) {
                        $FileSize = filesize($FilePath);
                        $TotalSize += $FileSize;
                        $Files[] = [
                            'Name' => $File,
                            'Path' => $FilePath,
                            'Size' => $FileSize,
                            'SizeFormatted' => self::FormatBytes($FileSize),
                            'ModifiedTime' => date('Y-m-d H:i:s', filemtime($FilePath)),
                            'IsCurrent' => $FilePath === $CurrentLogFile,
                        ];
                    }
                }
            }
        }

        // 按修改时间排序(最新的在前)
        usort($Files, function ($A, $B) {
            return filemtime($B['Path']) - filemtime($A['Path']);
        });

        return [
            'LogPath' => $LogPath,
            'CurrentLogFile' => $CurrentLogFile,
            'TotalFiles' => count($Files),
            'TotalSize' => $TotalSize,
            'TotalSizeFormatted' => self::FormatBytes($TotalSize),
            'MaxFileSize' => self::$Config['MaxFileSize'],
            'MaxFileSizeFormatted' => self::FormatBytes(self::$Config['MaxFileSize']),
            'MaxLogFiles' => self::$Config['MaxLogFiles'],
            'MinLevel' => self::$LevelNames[self::$Config['MinLevel']],
            'Files' => $Files,
        ];
    }

    /**
     * 格式化字节大小
     */
    private static function FormatBytes($Bytes)
    {
        $Units = ['B', 'KB', 'MB', 'GB'];
        $Bytes = max($Bytes, 0);
        $Pow = floor(($Bytes ? log($Bytes) : 0) / log(1024));
        $Pow = min($Pow, count($Units) - 1);
        $Bytes /= (1 << (10 * $Pow));

        return round($Bytes, 2) . ' ' . $Units[$Pow];
    }

    /**
     * 设置配置
     * 
     * @param array $Config 配置数组
     */
    public static function SetConfig($Config)
    {
        self::InitConfig();
        self::$Config = array_merge(self::$Config, $Config);
    }

    /**
     * 获取配置
     * 
     * @return array 配置数组
     */
    public static function GetConfig()
    {
        self::InitConfig();
        return self::$Config;
    }
}