Log rotate (#4032)

`server.log` is not rotated by default and grows unmanageably large, to the point where it's so huge that it's not possible to read it with any standard text editor anymore.

This PR implements automatic log rotation. 
- When the `server.log` reaches 32MB in size, it's moved to the `log_archive/` folder of the server's data folder.
- The archive's file name will look something like this: `server.2024-03-15T15.26.24.0.log`
- The file's name contains the date and time when the file was archived. This may be useful if you're trying to find logs from a particular time frame.

This has several benefits:
- Much more easily find logs from a particular time frame without scrolling through GBs of logs
- Free up space without stopping the server - Archived log files in `log_archive/` can be safely deleted and/or modified while the server is running

If you want to automatically compress or clean up the log files, I suggest an external cron job or disk watcher.

Closes #4029.
This commit is contained in:
Dylan T 2024-03-15 16:44:37 +00:00 committed by GitHub
parent e31fd122d9
commit 7148c7a222
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 84 additions and 11 deletions

View File

@ -326,7 +326,8 @@ JIT_WARNING
Terminal::init();
}
$logger = new MainLogger(Path::join($dataPath, "server.log"), Terminal::hasFormattingCodes(), "Server", new \DateTimeZone(Timezone::get()));
$logger = new MainLogger(Path::join($dataPath, "server.log"), Path::join($dataPath, "log_archive"), Terminal::hasFormattingCodes(), "Server", new \DateTimeZone(Timezone::get()));
\GlobalLogger::set($logger);
emit_performance_warnings($logger);

View File

@ -44,7 +44,7 @@ class MainLogger extends AttachableThreadSafeLogger implements \BufferedLogger{
/**
* @throws \RuntimeException
*/
public function __construct(string $logFile, bool $useFormattingCodes, string $mainThreadName, \DateTimeZone $timezone, bool $logDebug = false){
public function __construct(string $logFile, string $logArchiveDir, bool $useFormattingCodes, string $mainThreadName, \DateTimeZone $timezone, bool $logDebug = false){
parent::__construct();
$this->logDebug = $logDebug;
@ -52,7 +52,7 @@ class MainLogger extends AttachableThreadSafeLogger implements \BufferedLogger{
$this->mainThreadName = $mainThreadName;
$this->timezone = $timezone->getName();
$this->logWriterThread = new MainLoggerThread($logFile);
$this->logWriterThread = new MainLoggerThread($logFile, $logArchiveDir);
$this->logWriterThread->start(NativeThread::INHERIT_NONE);
}

View File

@ -25,23 +25,42 @@ namespace pocketmine\utils;
use pmmp\thread\Thread;
use pmmp\thread\ThreadSafeArray;
use function clearstatcache;
use function date;
use function fclose;
use function file_exists;
use function fopen;
use function fstat;
use function fwrite;
use function is_dir;
use function is_file;
use function is_resource;
use function mkdir;
use function pathinfo;
use function rename;
use function strlen;
use function touch;
use const PATHINFO_EXTENSION;
use const PATHINFO_FILENAME;
final class MainLoggerThread extends Thread{
/** @phpstan-var ThreadSafeArray<int, string> */
private ThreadSafeArray $buffer;
private bool $syncFlush = false;
private bool $shutdown = false;
public function __construct(
private string $logFile
private string $logFile,
private string $archiveDir,
private readonly int $maxFileSize = 32 * 1024 * 1024 //32 MB
){
$this->buffer = new ThreadSafeArray();
touch($this->logFile);
if(!@mkdir($this->archiveDir) && !is_dir($this->archiveDir)){
throw new \RuntimeException("Unable to create archive directory: " . (
is_file($this->archiveDir) ? "it already exists and is not a directory" : "permission denied"));
}
}
public function write(string $line) : void{
@ -71,12 +90,64 @@ final class MainLoggerThread extends Thread{
$this->join();
}
/** @return resource */
private function openLogFile(string $file, int &$size){
$logResource = fopen($file, "ab");
if(!is_resource($logResource)){
throw new \RuntimeException("Couldn't open log file");
}
$stat = fstat($logResource);
if($stat === false){
throw new AssumptionFailedError("fstat() should not fail here");
}
$size = $stat['size'];
return $logResource;
}
/**
* @param resource $logResource
* @return resource
*/
private function archiveLogFile($logResource, int &$size){
fclose($logResource);
clearstatcache();
$i = 0;
$date = date("Y-m-d\TH.i.s");
$baseName = pathinfo($this->logFile, PATHINFO_FILENAME);
$extension = pathinfo($this->logFile, PATHINFO_EXTENSION);
do{
//this shouldn't be necessary, but in case the user messes with the system time for some reason ...
$fileName = "$baseName.$date.$i.$extension";
$out = $this->archiveDir . "/" . $fileName;
$i++;
}while(file_exists($out));
//the user may have externally deleted the whole directory - make sure it exists before we do anything
@mkdir($this->archiveDir);
rename($this->logFile, $out);
$logResource = $this->openLogFile($this->logFile, $size);
fwrite($logResource, "--- Starting new log file - old log file archived as $fileName ---\n");
return $logResource;
}
private function logFileReadyToArchive(int $size) : bool{
return $size >= $this->maxFileSize;
}
/**
* @param resource $logResource
*/
private function writeLogStream($logResource) : void{
private function writeLogStream(&$logResource, int &$size) : void{
while(($chunk = $this->buffer->shift()) !== null){
fwrite($logResource, $chunk);
$size += strlen($chunk);
if($this->logFileReadyToArchive($size)){
$logResource = $this->archiveLogFile($logResource, $size);
}
}
$this->synchronized(function() : void{
@ -88,13 +159,14 @@ final class MainLoggerThread extends Thread{
}
public function run() : void{
$logResource = fopen($this->logFile, "ab");
if(!is_resource($logResource)){
throw new \RuntimeException("Couldn't open log file");
$size = 0;
$logResource = $this->openLogFile($this->logFile, $size);
if($this->logFileReadyToArchive($size)){
$logResource = $this->archiveLogFile($logResource, $size);
}
while(!$this->shutdown){
$this->writeLogStream($logResource);
$this->writeLogStream($logResource, $size);
$this->synchronized(function() : void{
if(!$this->shutdown && !$this->syncFlush){
$this->wait();
@ -102,7 +174,7 @@ final class MainLoggerThread extends Thread{
});
}
$this->writeLogStream($logResource);
$this->writeLogStream($logResource, $size);
fclose($logResource);
}

View File

@ -45,7 +45,7 @@ class AsyncPoolTest extends TestCase{
public function setUp() : void{
@define('pocketmine\\COMPOSER_AUTOLOADER_PATH', dirname(__DIR__, 3) . '/vendor/autoload.php');
$this->mainLogger = new MainLogger(tempnam(sys_get_temp_dir(), "pmlog"), false, "Main", new \DateTimeZone('UTC'));
$this->mainLogger = new MainLogger(tempnam(sys_get_temp_dir(), "pmlog"), sys_get_temp_dir(), false, "Main", new \DateTimeZone('UTC'));
$this->pool = new AsyncPool(2, 1024, new ThreadSafeClassLoader(), $this->mainLogger, new SleeperHandler());
}