PHP项目如何实现日志轮转?

wen java案例 1

本文目录导读:

PHP项目如何实现日志轮转?

  1. 使用PHP内置函数手动实现
  2. 使用Monolog库(推荐)
  3. 基于时间的轮转(按天轮转)
  4. 使用操作系统级日志轮转(推荐用于生产环境)
  5. 高级配置示例:完整的日志管理类
  6. 建议

在PHP项目中实现日志轮转(Log Rotation)有多种方法,以下是几种常见的实现方案:

使用PHP内置函数手动实现

<?php
class LogRotation {
    private $logFile;
    private $maxSize; // 字节
    private $maxFiles; // 保留的文件数
    public function __construct($logFile, $maxSize = 10485760, $maxFiles = 5) {
        $this->logFile = $logFile;
        $this->maxSize = $maxSize; // 默认10MB
        $this->maxFiles = $maxFiles;
    }
    public function write($message) {
        // 检查是否需要轮转
        if (file_exists($this->logFile) && filesize($this->logFile) >= $this->maxSize) {
            $this->rotate();
        }
        // 写入日志
        $timestamp = date('Y-m-d H:i:s');
        $logMessage = "[{$timestamp}] {$message}" . PHP_EOL;
        file_put_contents($this->logFile, $logMessage, FILE_APPEND | LOCK_EX);
    }
    private function rotate() {
        // 删除最旧的日志文件
        $oldestFile = $this->logFile . '.' . $this->maxFiles;
        if (file_exists($oldestFile)) {
            unlink($oldestFile);
        }
        // 轮转文件
        for ($i = $this->maxFiles - 1; $i >= 1; $i--) {
            $oldFile = $this->logFile . '.' . $i;
            $newFile = $this->logFile . '.' . ($i + 1);
            if (file_exists($oldFile)) {
                rename($oldFile, $newFile);
            }
        }
        // 将当前日志重命名为 .1
        rename($this->logFile, $this->logFile . '.1');
    }
}
// 使用示例
$logger = new LogRotation('/var/log/app.log', 10485760, 5);
$logger->write('这是一条测试日志');
?>

使用Monolog库(推荐)

Monolog是PHP最流行的日志库,内置日志轮转支持:

composer require monolog/monolog
<?php
require 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
// 创建日志实例
$log = new Logger('app');
// 创建轮转文件处理器
// 参数:日志文件路径,最大文件数,日志级别
$handler = new RotatingFileHandler(
    '/var/log/app.log',  // 日志文件路径
    7,                    // 保留最近7天的日志
    Logger::DEBUG        // 日志级别
);
// 设置日志文件名格式(可选)
$handler->setFilenameFormat(
    '{filename}-{date}',
    'Y-m-d'
);
$log->pushHandler($handler);
// 使用示例
$log->info('这是一条信息日志');
$log->error('这是一条错误日志', ['user_id' => 123]);
?>

基于时间的轮转(按天轮转)

<?php
class DailyLogRotation {
    private $logDir;
    private $logFilePrefix;
    private $retentionDays;
    public function __construct($logDir, $prefix = 'app', $retentionDays = 30) {
        $this->logDir = rtrim($logDir, '/');
        $this->logFilePrefix = $prefix;
        $this->retentionDays = $retentionDays;
        if (!is_dir($this->logDir)) {
            mkdir($this->logDir, 0755, true);
        }
    }
    public function write($message) {
        $todayLog = $this->getTodayLogFile();
        $timestamp = date('Y-m-d H:i:s');
        $logMessage = "[{$timestamp}] {$message}" . PHP_EOL;
        file_put_contents($todayLog, $logMessage, FILE_APPEND | LOCK_EX);
        // 清理过期日志
        $this->cleanOldLogs();
    }
    private function getTodayLogFile() {
        $date = date('Y-m-d');
        return "{$this->logDir}/{$this->logFilePrefix}-{$date}.log";
    }
    private function cleanOldLogs() {
        $files = glob("{$this->logDir}/{$this->logFilePrefix}-*.log");
        $now = time();
        foreach ($files as $file) {
            if (is_file($file)) {
                // 从文件名提取日期
                preg_match('/-(\d{4}-\d{2}-\d{2})\.log$/', $file, $matches);
                if (isset($matches[1])) {
                    $fileDate = strtotime($matches[1]);
                    $daysOld = ($now - $fileDate) / 86400;
                    if ($daysOld > $this->retentionDays) {
                        unlink($file);
                    }
                }
            }
        }
    }
}
// 使用示例
$logger = new DailyLogRotation('/var/log/app', 'app', 30);
$logger->write('用户登录成功');
?>

使用操作系统级日志轮转(推荐用于生产环境)

Linux logrotate配置

创建 /etc/logrotate.d/php-app

/var/log/php-app/*.log {
    daily                    # 每天轮转
    missingok               # 如果日志文件丢失,继续执行不报错
    rotate 7                # 保留7个备份
    compress               # 压缩旧日志
    delaycompress          # 延迟压缩(下一次轮转时压缩)
    notifempty            # 如果日志为空,不轮转
    create 0640 www-data www-data  # 创建新日志文件的权限和所有者
    sharedscripts          # 所有日志文件共享一个脚本
    postrotate
        /bin/kill -USR1 `cat /var/run/php-fpm.pid 2>/dev/null` 2>/dev/null || true
    endscript
}

高级配置示例:完整的日志管理类

<?php
class AdvancedLogger {
    private $logDir;
    private $currentLogFile;
    private $maxFileSize;
    private $maxFiles;
    private $dateFormat;
    public function __construct($config = []) {
        $this->logDir = $config['log_dir'] ?? '/var/log/app';
        $this->maxFileSize = $config['max_file_size'] ?? 10485760; // 10MB
        $this->maxFiles = $config['max_files'] ?? 5;
        $this->dateFormat = $config['date_format'] ?? 'Y-m-d H:i:s';
        if (!is_dir($this->logDir)) {
            mkdir($this->logDir, 0755, true);
        }
        $this->currentLogFile = $this->logDir . '/app.log';
    }
    public function log($level, $message, $context = []) {
        $this->checkRotation();
        $timestamp = date($this->dateFormat);
        $contextStr = !empty($context) ? ' ' . json_encode($context) : '';
        $logMessage = "[{$timestamp}] [{$level}] {$message}{$contextStr}" . PHP_EOL;
        // 写入文件
        file_put_contents($this->currentLogFile, $logMessage, FILE_APPEND | LOCK_EX);
        // 同时输出到控制台(开发环境)
        if (php_sapi_name() === 'cli') {
            echo $logMessage;
        }
    }
    private function checkRotation() {
        if (!file_exists($this->currentLogFile)) {
            return;
        }
        if (filesize($this->currentLogFile) >= $this->maxFileSize) {
            $this->rotate();
        }
    }
    private function rotate() {
        $info = pathinfo($this->currentLogFile);
        $base = $info['dirname'] . '/' . $info['filename'];
        // 删除最旧的文件
        $oldestFile = $base . '.' . $this->maxFiles . '.log';
        if (file_exists($oldestFile)) {
            unlink($oldestFile);
        }
        // 轮转文件
        for ($i = $this->maxFiles - 1; $i >= 1; $i--) {
            $oldFile = $base . '.' . $i . '.log';
            $newFile = $base . '.' . ($i + 1) . '.log';
            if (file_exists($oldFile)) {
                rename($oldFile, $newFile);
            }
        }
        // 压缩日志(可选)
        $logFile = $base . '.log';
        if (file_exists($logFile)) {
            rename($logFile, $base . '.1.log');
            $this->compressLog($base . '.1.log');
        }
    }
    private function compressLog($file) {
        if (function_exists('gzopen')) {
            $gzFile = $file . '.gz';
            $fp = fopen($file, 'r');
            $zp = gzopen($gzFile, 'w9');
            while (!feof($fp)) {
                gzwrite($zp, fread($fp, 8192));
            }
            fclose($fp);
            gzclose($zp);
            unlink($file);
        }
    }
    // 便捷方法
    public function info($message, $context = []) {
        $this->log('INFO', $message, $context);
    }
    public function error($message, $context = []) {
        $this->log('ERROR', $message, $context);
    }
    public function warning($message, $context = []) {
        $this->log('WARNING', $message, $context);
    }
    public function debug($message, $context = []) {
        $this->log('DEBUG', $message, $context);
    }
}
// 使用示例
$logger = new AdvancedLogger([
    'log_dir' => '/var/log/myapp',
    'max_file_size' => 5242880, // 5MB
    'max_files' => 10
]);
$logger->info('应用启动');
$logger->error('数据库连接失败', ['db' => 'mysql', 'host' => 'localhost']);
?>

建议

  1. 生产环境:推荐使用 logrotate 系统工具,性能更好
  2. 开发环境:使用 Monolog 或自定义轮转逻辑
  3. 重要考虑
    • 确保日志目录有适当的权限
    • 考虑使用异步写入提高性能
    • 监控磁盘空间使用情况
    • 设置合理的保留策略

选择哪种方案取决于你的具体需求、项目规模和部署环境。

抱歉,评论功能暂时关闭!