この記事で扱っているコードを試したLaravelのバージョンは10です。
LaravelはPHPのフレームワークでログを残すための機能が備わっています。このログ機能には日付単位でファイルを削除する機能があります。15日前のログファイルを削除すること常に14日分のログを残すとかそんな感じです。この機能は便利なのですが、ログファイルが巨大になると日付単位のローテートでは間に合わないくらいマシンの容量を占有してしまう危険性が残っています。マシンの空き容量がなくなると様々な機能が停止します。特にLinuxはファイルで様々なものを管理しているためファイルを追加できなくなると致命傷になりやすいです。そこでログの占めている容量を調べ、容量に合わせてログを自動削除するコマンドを作りたくなります。
ログを容量で削除するコマンドの実装例は次です。
<?php
namespace App\Console\Commands\Log;
use App\Console\BaseCommand;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
class RotateOldFiles extends BaseCommand
{
protected $signature = 'file:rotate-old-files
{--max-size-mb=4000 : 最大容量(MB)}
{--path=/storage/logs : 削除対象ディレクトリのパス}
{--pattern : 削除対象ファイルの名前パターン。デフォルトは*.log}
{--force : 確認なしでファイルを削除する}
';
protected $description = 'あるディレクトリ以下のファイルを古い順に削除して容量を減らす';
public function handle(): int
{
$tgtDirectory = base_path($this->option('path'));
$maxSizeByte = $this->option('max-size-mb') * 1024 * 1024;
if(!$this->option('force') && !$this->confirm("{$tgtDirectory} のファイルを {$this->option('max-size-mb')}MB 以下になるように古い順で削除しますか?")) {
return 0;
}
$removedFiles = $this->cleanOldFiles($tgtDirectory, $maxSizeByte);
if(empty($removedFiles)) {
$this->info('削除されたファイルはありませんでした');
return 0;
}
$this->info('以下のファイルを削除しました');
foreach ($removedFiles as $file) {
$this->warn($file);
}
return 0;
}
/**
* ディレクトリのサイズをバイト単位で取得する
* @param string $directory
* @return int
*/
function getDirectorySizeByte(string $directory): int
{
$size = 0;
// ディレクトリ以下を再帰的に回すイテレータを作成
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));
// ディレクトリ以下のファイル全てのサイズ見て合計する
foreach ($files as $file) {
if ($file->isFile()) {
$size += $file->getSize();
}
}
return $size;
}
/**
* @param string $directory
* @param int $maxSizeByte
* @return string[] 削除したファイルのパス
*/
private function cleanOldFiles(string $directory, int $maxSizeByte): array
{
$pattern = $this->option('pattern') ?: '*.log';
// ディレクトリ以下を再帰的に回すイテレータを配列化する
$files = iterator_to_array(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory)));
// パターンにマッチするファイルのみに絞り込む
$files = array_filter($files, fn ($file) => fnmatch($pattern, $file->getFilename()));
// ファイルを更新日時の昇順でソート
usort($files, fn ($a, $b) => $a->getMTime() - $b->getMTime());
// ディレクトリの現在のサイズを取得
$currentSize = $this->getDirectorySizeByte($directory);
// ファイルを更新日時が古い順に削除していく
$removedFiles = [];
foreach ($files as $file) {
if(!is_file($file)) {
continue;
}
if ($currentSize <= $maxSizeByte) {
// 最大容量より小さくなって入れば終了
break;
}
// ファイルを削除してサイズを更新&削除したファイルを記録
$currentSize -= $file->getSize();
$removedFiles[] = $file->getRealPath();
unlink($file->getRealPath());
}
return $removedFiles;
}
}
これを次のように手動実行すると古いログファイルを削除してくれます。
$ php artisan file:rotate-old-files --max-size-mb=10 --path=storage/logs /var/www/html/storage/logs のファイルを 10MB 以下になるように古い順で削除しますか? (yes/no) [no]: > yes 以下のファイルを削除しました /var/www/html/storage/logs/req_res-www-data-2024-05-14.log
次のようにLaravelのスケジューラに登録して定期実行させることもできます。
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule): void
{
$schedule->command('file:rotate-old-files', [
'--max-size-mb' => '4000',
'--path' => '/storage/logs',
'--force',
])->everyMinute(); // とりあえず毎分
}
}
このようにするとログファイルが不意に巨大化しても、すぐにログファイルが削除されるため問題が起きにくくなります。