I've been using this Log class to conveniently write to a log such that the log file will not grow without bound. The class takes care of rotating the log file and keeping a few backups so that the file never gets too big. I was considering using it in a multiprocessing application -- i.e., there are many processes potentially trying to write the same exact log file. If that log file happens to be in need of rotation, there is the potential for exceptions to be thrown if two processes are trying to write and/or rotate the log at the same time. The odds are pretty long but it could happen.
Can anyone suggest how I might improve this log class such that many processes can simultaneously write to the same log file without the possibility of processes trying to write a file that is being rotated or rotate a file that is being written?
<?php
/**
* A class to simplify log management. Easy to instantiate a logging object
* which will take care to rotate logs when the current log becomes large
* so we don't have to worry about logs growing without bound and using
* up all the hard drive space.
*/
class Log {
/**
* The maximum filesize (in bytes) permitted for a log.
* I have set it here to 100MB
* @var int
*/
const MAX_LOG_SIZE = 100000000;
/**
* The number of old copies of the file we keep
* @var int
*/
const BACKUP_FILE_COUNT = 5;
/**
* The path and filename of the log file
* E.g., /path/to/file.log
* @var string
*/
private $full_log_file_path = NULL;
/**
* Path to the directory where these log files are getting saved
* @var string
*/
private $log_dir = NULL;
/**
* The basename of the log file path.
* E.g., file.log
* @var string
*/
private $basename = NULL;
/**
* constructor, duh. creates an instance of this class
* you can happily use for logging. This routine parses the provided log path
* into parts in order to manage log rotation, etc.
* @param string $full_log_file_path The path and filename of the main log file to be written
*/
public function __construct($full_log_file_path) {
$this->full_log_file_path = $full_log_file_path;
$path_parts = pathinfo($full_log_file_path);
$this->log_dir = $path_parts["dirname"];
$this->basename = $path_parts["basename"];
// if no path was specified, assume CWD:
if (strlen($this->log_dir) == 0){
$this->log_dir = ".";
}
// validation / error checking
if (!$this->basename || strlen($this->basename) == 0){
throw new Exception("Unable to create Log object. Basename is not valid");
}
clearstatcache();
if (!is_writable($this->log_dir)){
throw new Exception("Log constructor failed. Log directory (" . $this->log_dir . ") is not writable");
}
}
/**
* This function rotates the logs, deleting
* one if we have reached our limit. It's called
* when our current log file has met or exceeded
* self::MAX_LOG_SIZE
*/
function rotate_logs() {
//delete the last log if it exists
$last_log = $this->get_log_filename(self::BACKUP_FILE_COUNT);
if (file_exists($last_log)) {
if (!unlink($last_log)) {
throw new Exception("Log::rotate_logs failed. Unable to delete last log (" . $last_log . ")");
}
}
// rotate the other backup logs
for($i=self::BACKUP_FILE_COUNT; $i>1; $i--) {
$old_log = $this->get_log_filename($i - 1);
$new_log = $this->get_log_filename($i);
if (file_exists($old_log)) {
rename($old_log, $new_log);
}
}
// rename the main log file to .1
$current_log = $this->get_log_filename();
$new_log = $this->get_log_filename(1);
if (file_exists($current_log)) {
rename($current_log, $new_log);
}
}
/**
* Returns a complete path to either the main log file
* or, if optional argument $num is supplied, returns
* the full path to one of the numbered backup files
* (e.g., /path/to/file.log.1)
* @param int $num
*/
function get_log_filename($num=NULL) {
if (is_null($num)) {
return $this->full_log_file_path;
} else {
return $this->full_log_file_path . "." . $num;
}
} // get_log_filename()
/**
* This routine writes a message to the map log, taking care to
* rotate it first if the resulting file would exceed the threshold size
*/
function write($msg) {
clearstatcache();
$msg_length = strlen($msg);
if ($msg_length > self::MAX_LOG_SIZE) {
throw new Exception("Unable to write log message to " . $this->full_log_file_path . " because the message length (" . $msg_length . ") exceeds the maximum log size (" . self::MAX_LOG_SIZE . ")");
}
if (file_exists($this->full_log_file_path)) {
if (!is_writable($this->full_log_file_path)) {
throw new Exception("Unable to write log file. " . $this->full_log_file_path . " is not writable");
}
$pending_file_size = filesize($this->full_log_file_path) + $msg_length;
if ($pending_file_size > self::MAX_LOG_SIZE){
$this->rotate_logs();
}
} elseif (!is_writable($this->log_dir)) {
throw new Exception("Unable to create log file because directory " . $this->log . " is not writable");
}
file_put_contents($this->full_log_file_path, "[" . date("Y-m-d H:i:s") . "] - " . $msg . "\n", FILE_APPEND);
} // write()
} // class Log