本文目录导读:

在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');
- 推荐使用logrotate:系统级工具,稳定高效
- PHP实现:适合没有系统权限的环境
- Monolog:专业日志库,功能完整
- 按需选择:根据项目需求和运维环境决定
选择方案时考虑:服务器负载、日志量大小、运维能力、是否需要实时压缩等因素。