PHP项目日志归档如何按周期压缩转储文件

wen PHP项目 25

本文目录导读:

PHP项目日志归档如何按周期压缩转储文件

  1. 使用Linux日志轮转工具(推荐)
  2. PHP内置日志轮转实现
  3. 使用Monolog日志库(推荐)
  4. 按周/月轮转实现
  5. 最佳实践建议

在PHP项目中进行日志归档和按周期压缩转储,通常有几种常见方案,以下是完整的实现方法:

使用Linux日志轮转工具(推荐)

logrotate配置

# /etc/logrotate.d/php-app
/var/log/php-app/*.log {
    daily                    # 按天轮转
    rotate 30                # 保留30份
    compress                 # 压缩旧日志
    delaycompress            # 延迟压缩(保留最新一份不压缩)
    missingok                # 日志不存在不报错
    notifempty               # 空文件不轮转
    dateext                  # 使用日期后缀
    dateformat -%Y%m%d       # 日期格式
    copytruncate             # 复制并截断原文件(不影响PHP写入)
    postrotate
        # 可选:重启PHP进程释放文件句柄
        # systemctl reload php-fpm
    endscript
}

手动测试配置

sudo logrotate -d /etc/logrotate.d/php-app  # 调试模式
sudo logrotate -f /etc/logrotate.d/php-app  # 强制轮转

PHP内置日志轮转实现

简单版:按文件大小轮转

<?php
class LogRotator {
    private $logFile;
    private $maxSize;
    private $maxFiles;
    private $compress;
    public function __construct($logFile, $maxSize = 104857600, $maxFiles = 30, $compress = true) {
        $this->logFile = $logFile;
        $this->maxSize = $maxSize; // 100MB默认
        $this->maxFiles = $maxFiles;
        $this->compress = $compress;
    }
    public function write($message) {
        file_put_contents($this->logFile, $message . PHP_EOL, FILE_APPEND | LOCK_EX);
        if (filesize($this->logFile) > $this->maxSize) {
            $this->rotate();
        }
    }
    private function rotate() {
        // 删除最旧文件
        $oldestFile = $this->logFile . '.' . $this->maxFiles . '.gz';
        if (file_exists($oldestFile)) {
            unlink($oldestFile);
        }
        // 重命名并压缩旧文件
        for ($i = $this->maxFiles - 1; $i >= 1; $i--) {
            $oldFile = $this->logFile . '.' . $i;
            $oldFileGz = $oldFile . '.gz';
            // 解压后的文件
            if (file_exists($oldFileGz)) {
                if ($i == $this->maxFiles - 1) {
                    unlink($oldFileGz);
                } else {
                    rename($oldFileGz, $this->logFile . '.' . ($i + 1) . '.gz');
                }
            }
            // 直接压缩的文件
            if (file_exists($oldFile)) {
                if ($this->compress) {
                    $this->compressFile($oldFile);
                    rename($oldFile . '.gz', $this->logFile . '.' . ($i + 1) . '.gz');
                } else {
                    rename($oldFile, $this->logFile . '.' . ($i + 1));
                }
            }
        }
        // 当前日志文件编号为1
        copy($this->logFile, $this->logFile . '.1');
        if ($this->compress) {
            $this->compressFile($this->logFile . '.1');
            rename($this->logFile . '.1.gz', $this->logFile . '.1.gz');
        }
        // 清空当前日志
        file_put_contents($this->logFile, '');
    }
    private function compressFile($file) {
        $data = file_get_contents($file);
        $gzData = gzencode($data, 9);
        file_put_contents($file . '.gz', $gzData);
        unlink($file);
    }
}

高级版:按日期轮转

<?php
class DateBasedLogRotator {
    private $logFile;
    private $retentionDays;
    private $compress;
    private $dateFormat;
    public function __construct($logFile, $retentionDays = 30, $compress = true) {
        $this->logFile = $logFile;
        $this->retentionDays = $retentionDays;
        $this->compress = $compress;
        $this->dateFormat = 'Y-m-d';
    }
    public function write($message) {
        $today = date($this->dateFormat);
        $currentDate = $this->getCurrentDate();
        if ($currentDate !== $today) {
            $this->archive();
        }
        file_put_contents($this->logFile, $message . PHP_EOL, FILE_APPEND | LOCK_EX);
    }
    private function archive() {
        if (!file_exists($this->logFile)) return;
        $archiveName = $this->logFile . '.' . date($this->dateFormat, strtotime('-1 day'));
        if ($this->compress) {
            $data = file_get_contents($this->logFile);
            $gzData = gzencode($data, 9);
            file_put_contents($archiveName . '.gz', $gzData);
        } else {
            rename($this->logFile, $archiveName);
        }
        file_put_contents($this->logFile, '');
        $this->updateDateFile();
        $this->cleanupOldFiles();
    }
    private function cleanupOldFiles() {
        $pattern = $this->logFile . '.*';
        $files = glob($pattern);
        foreach ($files as $file) {
            // 提取日期
            preg_match('/\.(\d{4}-\d{2}-\d{2})/', $file, $matches);
            if (isset($matches[1])) {
                $fileDate = strtotime($matches[1]);
                if ($fileDate < strtotime("-{$this->retentionDays} days")) {
                    unlink($file);
                }
            }
        }
    }
    private function getCurrentDate() {
        $dateFile = $this->logFile . '.date';
        if (file_exists($dateFile)) {
            return file_get_contents($dateFile);
        }
        return date($this->dateFormat);
    }
    private function updateDateFile() {
        $dateFile = $this->logFile . '.date';
        file_put_contents($dateFile, date($this->dateFormat));
    }
}

使用Monolog日志库(推荐)

安装

composer require monolog/monolog

配置轮转

<?php
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Formatter\LineFormatter;
$log = new Logger('app');
// 按天轮转,保留30天,压缩
$handler = new RotatingFileHandler('/var/log/php-app/app.log', 30, Logger::DEBUG);
$handler->setFilenameFormat('{filename}-{date}', 'Y-m-d'); // 自定义文件名格式
$handler->setFormatter(new LineFormatter(null, null, true, true));
// 启用压缩
$handler->setRotationCompress(true); // 自动压缩为.gz
$log->pushHandler($handler);
// 使用
$log->info('This is a log message', ['context' => 'value']);

按周/月轮转实现

自定义轮转周期

<?php
class CustomPeriodLogRotator {
    private $logFile;
    private $period; // 'daily', 'weekly', 'monthly'
    private $retention;
    private $compress;
    public function __construct($logFile, $period = 'daily', $retention = 30, $compress = true) {
        $this->logFile = $logFile;
        $this->period = $period;
        $this->retention = $retention;
        $this->compress = $compress;
    }
    public function checkAndRotate() {
        $currentPeriod = $this->getCurrentPeriod();
        $storedPeriod = $this->getStoredPeriod();
        if ($currentPeriod !== $storedPeriod) {
            $this->doRotate($storedPeriod);
            $this->saveCurrentPeriod($currentPeriod);
        }
    }
    private function getCurrentPeriod() {
        switch ($this->period) {
            case 'daily':
                return date('Y-m-d');
            case 'weekly':
                return date('Y-W');
            case 'monthly':
                return date('Y-m');
            default:
                return date('Y-m-d');
        }
    }
    private function doRotate($oldPeriod) {
        if (!file_exists($this->logFile)) return;
        $archiveName = $this->logFile . '.' . $oldPeriod;
        if ($this->compress) {
            $data = file_get_contents($this->logFile);
            file_put_contents($archiveName . '.gz', gzencode($data, 9));
        } else {
            copy($this->logFile, $archiveName);
        }
        file_put_contents($this->logFile, '');
        $this->cleanup();
    }
    private function cleanup() {
        $files = glob($this->logFile . '.*');
        $now = time();
        foreach ($files as $file) {
            $fileTime = filemtime($file);
            $ageInSeconds = $now - $fileTime;
            $retentionInSeconds = $this->retention * 24 * 3600;
            if ($ageInSeconds > $retentionInSeconds) {
                unlink($file);
            }
        }
    }
    private function getStoredPeriod() {
        $metaFile = $this->logFile . '.meta';
        if (file_exists($metaFile)) {
            return file_get_contents($metaFile);
        }
        return $this->getCurrentPeriod();
    }
    private function saveCurrentPeriod($period) {
        file_put_contents($this->logFile . '.meta', $period);
    }
}

最佳实践建议

生产环境配置

<?php
// config/log.php
return [
    'default' => [
        'driver' => 'daily',
        'path' => storage_path('logs/app.log'),
        'level' => 'debug',
        'days' => 30,
        'compress' => true,
    ],
    // 分离错误日志
    'error' => [
        'driver' => 'daily',
        'path' => storage_path('logs/error.log'),
        'level' => 'error',
        'days' => 60,
        'compress' => true,
    ],
];

定时任务(Crontab)

# 每天凌晨执行日志轮转检查
0 0 * * * /usr/bin/php /path/to/rotate.php >> /var/log/php-rotate.log 2>&1

rotate.php脚本

<?php
require 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
$logger = new Logger('rotate');
$handler = new RotatingFileHandler('/var/log/php-app/app.log', 30, Logger::DEBUG);
$handler->setRotationCompress(true);
$logger->pushHandler($handler);
// 触发轮转(Monolog自动处理)
$logger->info('Log rotation check completed');
  1. 推荐使用logrotate:系统级工具,稳定高效
  2. PHP实现:适合没有系统权限的环境
  3. Monolog:专业日志库,功能完整
  4. 按需选择:根据项目需求和运维环境决定

选择方案时考虑:服务器负载、日志量大小、运维能力、是否需要实时压缩等因素。

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