zoneminder/web/includes/logger.php

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

536 lines
16 KiB
PHP
Raw Permalink Normal View History

<?php
namespace ZM;
2020-05-11 21:19:33 +08:00
require_once('config.php');
2017-10-21 02:54:28 +08:00
class Logger {
private static $instance;
const DEBUG = 1;
const INFO = 0;
const WARNING = -1;
const ERROR = -2;
const FATAL = -3;
const PANIC = -4;
const NOLOG = -5; // Special artificial level to prevent logging
private $initialised = false;
private $id = 'web';
private $idRoot = 'web';
private $idArgs = '';
2017-10-21 02:54:28 +08:00
private $useErrorLog = true;
private $level = self::INFO;
private $termLevel = self::NOLOG;
private $databaseLevel = self::NOLOG;
private $fileLevel = self::NOLOG;
private $weblogLevel = self::NOLOG;
private $syslogLevel = self::NOLOG;
private $effectiveLevel = self::NOLOG;
private $hasTerm = false;
private $logPath = ZM_PATH_LOGS;
private $logFile = '';
2017-10-21 02:54:28 +08:00
private $logFd = NULL;
public static $codes = array(
self::DEBUG => 'DBG',
self::INFO => 'INF',
self::WARNING => 'WAR',
self::ERROR => 'ERR',
self::FATAL => 'FAT',
self::PANIC => 'PNC',
self::NOLOG => 'OFF',
2017-10-21 02:54:28 +08:00
);
private static $syslogPriorities = array(
self::DEBUG => LOG_DEBUG,
self::INFO => LOG_INFO,
self::WARNING => LOG_WARNING,
self::ERROR => LOG_ERR,
self::FATAL => LOG_ERR,
self::PANIC => LOG_ERR,
);
private static $phpErrorLevels = array(
self::DEBUG => E_USER_NOTICE,
self::INFO => E_USER_NOTICE,
self::WARNING => E_USER_WARNING,
self::ERROR => E_USER_WARNING,
self::FATAL => E_USER_ERROR,
self::PANIC => E_USER_ERROR,
);
private function __construct() {
$this->hasTerm = (php_sapi_name() == 'cli' && empty($_SERVER['REMOTE_ADDR']));
$this->logFile = $this->logPath.'/'.$this->id.'.log';
2017-10-21 02:54:28 +08:00
}
public function __destruct() {
$this->terminate();
}
2020-05-11 21:19:33 +08:00
public function initialise($options=array()) {
2017-10-21 02:54:28 +08:00
if ( !empty($options['id']) )
$this->id = $options['id'];
//if ( isset($options['useErrorLog']) )
//$this->useErrorLog = $options['useErrorLog'];
if ( isset($options['logPath']) ) {
$this->logPath = $options['logPath'];
$tempLogFile = $this->logPath.'/'.$this->id.'.log';
}
2017-10-21 02:54:28 +08:00
if ( isset($options['logFile']) )
$tempLogFile = $options['logFile'];
else
$tempLogFile = $this->logPath.'/'.$this->id.'.log';
2017-10-21 02:54:28 +08:00
if ( !is_null($logFile = $this->getTargettedEnv('LOG_FILE')) )
$tempLogFile = $logFile;
$tempLevel = self::INFO;
$tempTermLevel = $this->termLevel;
$tempDatabaseLevel = $this->databaseLevel;
$tempFileLevel = $this->fileLevel;
$tempSyslogLevel = $this->syslogLevel;
$tempWeblogLevel = $this->weblogLevel;
if ( isset($options['termLevel']) )
$tempTermLevel = $options['termLevel'];
if ( isset($options['databaseLevel']) )
$tempDatabaseLevel = $options['databaseLevel'];
else
$tempDatabaseLevel = ZM_LOG_LEVEL_DATABASE;
2017-10-21 02:54:28 +08:00
if ( isset($options['fileLevel']) )
$tempFileLevel = $options['fileLevel'];
else
$tempFileLevel = ZM_LOG_LEVEL_FILE;
2017-10-21 02:54:28 +08:00
if ( isset($options['weblogLevel']) )
$tempWeblogLevel = $options['weblogLevel'];
else
$tempWeblogLevel = ZM_LOG_LEVEL_WEBLOG;
2017-10-21 02:54:28 +08:00
if ( isset($options['syslogLevel']) )
$tempSyslogLevel = $options['syslogLevel'];
else
$tempSyslogLevel = ZM_LOG_LEVEL_SYSLOG;
if ( $value = getenv('LOG_PRINT') )
$tempTermLevel = $value ? self::DEBUG : self::NOLOG;
if ( !is_null($level = $this->getTargettedEnv('LOG_LEVEL')) )
$tempLevel = $level;
if ( !is_null($level = $this->getTargettedEnv('LOG_LEVEL_TERM')) )
$tempTermLevel = $level;
if ( !is_null($level = $this->getTargettedEnv('LOG_LEVEL_DATABASE')) )
$tempDatabaseLevel = $level;
if ( !is_null($level = $this->getTargettedEnv('LOG_LEVEL_FILE')) )
$tempFileLevel = $level;
if ( !is_null($level = $this->getTargettedEnv('LOG_LEVEL_SYSLOG')) )
$tempSyslogLevel = $level;
if ( !is_null($level = $this->getTargettedEnv('LOG_LEVEL_WEBLOG')) )
$tempWeblogLevel = $level;
if ( ZM_LOG_DEBUG ) {
foreach ( explode( '|', ZM_LOG_DEBUG_TARGET ) as $target ) {
if ( $target == $this->id || $target == '_'.$this->id || $target == $this->idRoot || $target == '_'.$this->idRoot || $target == '' ) {
if ( ZM_LOG_DEBUG_LEVEL > self::NOLOG ) {
$tempLevel = $this->limit( ZM_LOG_DEBUG_LEVEL );
if ( ZM_LOG_DEBUG_FILE != '' ) {
$tempLogFile = ZM_LOG_DEBUG_FILE;
$tempFileLevel = $tempLevel;
}
2017-10-21 02:54:28 +08:00
}
}
2017-10-21 02:54:28 +08:00
} // end foreach target
} // end if DEBUG
2017-10-21 02:54:28 +08:00
$this->logFile( $tempLogFile );
$this->termLevel( $tempTermLevel );
$this->databaseLevel( $tempDatabaseLevel );
$this->fileLevel( $tempFileLevel );
$this->syslogLevel( $tempSyslogLevel );
$this->weblogLevel( $tempWeblogLevel );
2017-10-21 02:54:28 +08:00
$this->level( $tempLevel );
2017-10-21 02:54:28 +08:00
$this->initialised = true;
//Debug( "LogOpts: level=".self::$codes[$this->level]."/".self::$codes[$this->effectiveLevel].", screen=".self::$codes[$this->termLevel].", database=".self::$codes[$this->databaseLevel].", logfile=".self::$codes[$this->fileLevel]."->".$this->logFile.", weblog=".self::$codes[$this->weblogLevel].", syslog=".self::$codes[$this->syslogLevel] );
2017-10-21 02:54:28 +08:00
}
2017-10-21 02:54:28 +08:00
private function terminate() {
if ( $this->initialised ) {
if ( $this->fileLevel > self::NOLOG )
$this->closeFile();
if ( $this->syslogLevel > self::NOLOG )
$this->closeSyslog();
}
2017-10-21 02:54:28 +08:00
$this->initialised = false;
}
private function limit( $level ) {
if ( $level > self::DEBUG )
return( self::DEBUG );
if ( $level < self::NOLOG )
return( self::NOLOG );
return( $level );
}
private function getTargettedEnv( $name ) {
$envName = $name.'_'.$this->id;
2017-10-21 02:54:28 +08:00
$value = getenv( $envName );
if ( $value === false && $this->id != $this->idRoot )
$value = getenv( $name.'_'.$this->idRoot );
2017-10-21 02:54:28 +08:00
if ( $value === false )
$value = getenv( $name );
return( $value !== false ? $value : NULL );
}
public static function fetch( $initialise=true ) {
if ( !isset(self::$instance) ) {
$class = __CLASS__;
self::$instance = new $class;
if ( $initialise )
self::$instance->initialise( array( 'id'=>'web_php', 'syslogLevel'=>self::INFO, 'weblogLevel'=>self::INFO ) );
}
2017-10-21 02:54:28 +08:00
return self::$instance;
}
public function id( $id=NULL ) {
if ( isset($id) && $this->id != $id ) {
// Remove whitespace
$id = preg_replace( '/\S/', '', $id );
// Replace non-alphanum with underscore
$id = preg_replace( '/[^a-zA-Z_]/', '_', $id );
if ( $this->id != $id ) {
$this->id = $this->idRoot = $id;
if ( preg_match( '/^([^_]+)_(.+)$/', $id, $matches ) ) {
$this->idRoot = $matches[1];
$this->idArgs = $matches[2];
}
2017-10-21 02:54:28 +08:00
}
}
2020-05-06 06:07:28 +08:00
return $this->id;
2017-10-21 02:54:28 +08:00
}
public function level( $level ) {
if ( isset($level) ) {
$lastLevel = $this->level;
$this->level = $this->limit($level);
$this->effectiveLevel = self::NOLOG;
if ( $this->termLevel > $this->effectiveLevel )
$this->effectiveLevel = $this->termLevel;
if ( $this->databaseLevel > $this->effectiveLevel )
$this->effectiveLevel = $this->databaseLevel;
if ( $this->fileLevel > $this->effectiveLevel )
$this->effectiveLevel = $this->fileLevel;
if ( $this->weblogLevel > $this->effectiveLevel )
$this->effectiveLevel = $this->weblogLevel;
if ( $this->syslogLevel > $this->effectiveLevel )
$this->effectiveLevel = $this->syslogLevel;
if ( $this->effectiveLevel > $this->level )
$this->effectiveLevel = $this->level;
if ( !$this->hasTerm ) {
if ( $lastLevel < self::DEBUG && $this->level >= self::DEBUG ) {
2020-05-11 21:19:33 +08:00
$this->savedErrorReporting = error_reporting(E_ALL);
$this->savedDisplayErrors = ini_set('display_errors', true);
2017-10-21 02:54:28 +08:00
} elseif ( $lastLevel >= self::DEBUG && $this->level < self::DEBUG ) {
2020-05-11 21:19:33 +08:00
error_reporting($this->savedErrorReporting);
ini_set('display_errors', $this->savedDisplayErrors);
}
2017-10-21 02:54:28 +08:00
}
}
2020-05-11 21:19:33 +08:00
return $this->level;
2017-10-21 02:54:28 +08:00
}
public function debugOn() {
return( $this->effectiveLevel >= self::DEBUG );
}
2020-05-11 21:19:33 +08:00
public function termLevel($termLevel) {
2017-10-21 02:54:28 +08:00
if ( isset($termLevel) ) {
$termLevel = $this->limit($termLevel);
if ( $this->termLevel != $termLevel )
$this->termLevel = $termLevel;
}
2020-05-11 21:19:33 +08:00
return $this->termLevel;
2017-10-21 02:54:28 +08:00
}
2020-05-11 21:19:33 +08:00
public function databaseLevel($databaseLevel=NULL) {
2017-10-21 02:54:28 +08:00
if ( !is_null($databaseLevel) ) {
$databaseLevel = $this->limit($databaseLevel);
if ( $this->databaseLevel != $databaseLevel ) {
$this->databaseLevel = $databaseLevel;
if ( $this->databaseLevel > self::NOLOG ) {
if ( (include_once 'database.php') === FALSE ) {
$this->databaseLevel = self::NOLOG;
2020-05-11 21:19:33 +08:00
Warning('Unable to write log entries to DB, database.php not found');
2017-10-21 02:54:28 +08:00
}
}
2017-10-21 02:54:28 +08:00
}
}
return $this->databaseLevel;
2017-10-21 02:54:28 +08:00
}
2020-05-11 21:19:33 +08:00
public function fileLevel($fileLevel) {
2017-10-21 02:54:28 +08:00
if ( isset($fileLevel) ) {
$fileLevel = $this->limit($fileLevel);
if ( $this->fileLevel != $fileLevel ) {
if ( $this->fileLevel > self::NOLOG )
$this->closeFile();
$this->fileLevel = $fileLevel;
if ( $this->fileLevel > self::NOLOG )
$this->openFile();
}
}
return $this->fileLevel;
2017-10-21 02:54:28 +08:00
}
2020-05-11 21:19:33 +08:00
public function weblogLevel($weblogLevel) {
2017-10-21 02:54:28 +08:00
if ( isset($weblogLevel) ) {
$weblogLevel = $this->limit($weblogLevel);
if ( $this->weblogLevel != $weblogLevel ) {
if ( $weblogLevel > self::NOLOG && $this->weblogLevel <= self::NOLOG ) {
2020-05-11 21:19:33 +08:00
$this->savedLogErrors = ini_set('log_errors', true);
2017-10-21 02:54:28 +08:00
} elseif ( $weblogLevel <= self::NOLOG && $this->weblogLevel > self::NOLOG ) {
2020-05-11 21:19:33 +08:00
ini_set('log_errors', $this->savedLogErrors);
}
2017-10-21 02:54:28 +08:00
$this->weblogLevel = $weblogLevel;
}
}
return $this->weblogLevel;
2017-10-21 02:54:28 +08:00
}
2020-05-11 21:19:33 +08:00
public function syslogLevel($syslogLevel) {
2017-10-21 02:54:28 +08:00
if ( isset($syslogLevel) ) {
$syslogLevel = $this->limit($syslogLevel);
if ( $this->syslogLevel != $syslogLevel ) {
if ( $this->syslogLevel > self::NOLOG )
$this->closeSyslog();
$this->syslogLevel = $syslogLevel;
if ( $this->syslogLevel > self::NOLOG )
$this->openSyslog();
}
}
return $this->syslogLevel;
2017-10-21 02:54:28 +08:00
}
2017-10-21 02:54:28 +08:00
private function openSyslog() {
openlog($this->id, LOG_PID|LOG_NDELAY, LOG_LOCAL1);
2017-10-21 02:54:28 +08:00
}
2017-10-21 02:54:28 +08:00
private function closeSyslog() {
closelog();
}
private function logFile($logFile) {
if ( preg_match('/^(.+)\+$/', $logFile, $matches) ) {
2017-10-21 02:54:28 +08:00
$this->logFile = $matches[1].'.'.getmypid();
} else {
2017-10-21 02:54:28 +08:00
$this->logFile = $logFile;
}
2017-10-21 02:54:28 +08:00
}
private function openFile() {
if ( !$this->useErrorLog ) {
if ( $this->logFd = fopen($this->logFile, 'a+') ) {
if ( strnatcmp(phpversion(), '5.2.0') >= 0 ) {
2017-10-21 02:54:28 +08:00
$error = error_get_last();
trigger_error("Can't open log file '$logFile': ".$error['message'].' @ '.$error['file'].'/'.$error['line'], E_USER_ERROR);
}
2017-10-21 02:54:28 +08:00
$this->fileLevel = self::NOLOG;
}
}
2017-10-21 02:54:28 +08:00
}
private function closeFile() {
if ( $this->logFd )
fclose($this->logFd);
2017-10-21 02:54:28 +08:00
}
2020-05-11 21:19:33 +08:00
public function logPrint($level, $string, $file=NULL, $line=NULL) {
if ( $level > $this->effectiveLevel ) {
return;
}
$string = preg_replace('/[\r\n]+$/', '', $string);
$code = self::$codes[$level];
$time = gettimeofday();
$message = sprintf('%s.%06d %s[%d].%s [%s] [%s]',
strftime('%x %H:%M:%S', $time['sec']), $time['usec'],
$this->id, getmypid(), $code, $_SERVER['REMOTE_ADDR'], $string);
if ( is_null($file) ) {
if ( $this->useErrorLog || ($this->databaseLevel > self::NOLOG) ) {
$backTrace = debug_backtrace();
$file = $backTrace[1]['file'];
$line = $backTrace[1]['line'];
2017-10-21 02:54:28 +08:00
if ( $this->hasTerm )
$rootPath = getcwd();
else
$rootPath = $_SERVER['DOCUMENT_ROOT'];
2020-05-11 21:19:33 +08:00
$file = preg_replace('/^'.addcslashes($rootPath, '/').'\/?/', '', $file);
}
}
if ( $this->useErrorLog ) {
$message .= ' at '.$file.' line '.$line;
}
if ( $level <= $this->termLevel ) {
2020-05-11 21:19:33 +08:00
if ( $this->hasTerm ) {
print($message."\n");
2020-05-11 21:19:33 +08:00
} else {
// Didn't we already replace all newlines with spaces above?
print(preg_replace('/\n/', '<br/>', htmlspecialchars($message)).'<br/>');
}
}
if ( $level <= $this->fileLevel ) {
if ( $this->useErrorLog ) {
if ( !error_log($message."\n", 3, $this->logFile) ) {
if ( strnatcmp(phpversion(), '5.2.0') >= 0 ) {
$error = error_get_last();
$this->fileLevel = self::NOLOG;
Error("Can't write to log file '".$this->logFile."': ".$error['message'].' @ '.$error['file'].'/'.$error['line'], E_USER_ERROR);
2017-10-21 02:54:28 +08:00
}
}
} else if ( $this->logFd ) {
fprintf($this->logFd, $message."\n");
2020-05-11 21:19:33 +08:00
} else {
$this->fileLevel = self::NOLOG;
Error('No logFd but have fileLevel logging!?');
}
}
if ( $level <= $this->syslogLevel )
syslog(self::$syslogPriorities[$level], $message);
$message = $code.' ['.$string.']';
if ( $level <= $this->databaseLevel ) {
if ( strlen($file) > 255 )
$file = substr($file, 0, 255);
try {
global $dbConn;
$sql = 'INSERT INTO `Logs` ( `TimeKey`, `Component`, `ServerId`, `Pid`, `Level`, `Code`, `Message`, `File`, `Line` ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ? )';
$stmt = $dbConn->prepare($sql);
$result = $stmt->execute(array(sprintf('%d.%06d', $time['sec'], $time['usec']), $this->id,
(defined('ZM_SERVER_ID') ? ZM_SERVER_ID : null), getmypid(), $level, $code, $string, $file, $line));
} catch(PDOException $ex) {
$this->databaseLevel = self::NOLOG;
Error("Can't write log entry '$sql': ". $ex->getMessage());
2017-10-21 02:54:28 +08:00
}
}
// This has to be last as trigger_error can be fatal
if ( $level <= $this->weblogLevel ) {
if ( $this->useErrorLog ) {
error_log($message, 0);
} else {
trigger_error($message, self::$phpErrorLevels[$level]);
2017-10-21 02:54:28 +08:00
}
}
2017-10-21 02:54:28 +08:00
}
};
2017-10-21 02:54:28 +08:00
function logInit( $options=array() ) {
$logger = Logger::fetch();
$logger->initialise( $options );
set_error_handler( 'ZM\ErrorHandler' );
}
2017-10-21 02:54:28 +08:00
function logToDatabase( $level=NULL ) {
return( Logger::fetch()->databaseLevel( $level ) );
}
2017-10-21 02:54:28 +08:00
function Mark( $level=Logger::DEBUG, $tag='Mark' ) {
Logger::fetch()->logPrint( $level, $tag );
}
2017-10-21 02:54:28 +08:00
function Dump( &$var, $label='VAR' ) {
ob_start();
print( $label.' => ' );
print_r( $var );
Logger::fetch()->logPrint( Logger::DEBUG, ob_get_clean() );
}
function Debug( $string ) {
Logger::fetch()->logPrint( Logger::DEBUG, $string );
}
2017-10-21 02:54:28 +08:00
function Info( $string ) {
Logger::fetch()->logPrint( Logger::INFO, $string );
}
2017-10-21 02:54:28 +08:00
function Warning( $string ) {
Logger::fetch()->logPrint( Logger::WARNING, $string );
}
2017-10-21 02:54:28 +08:00
function Error( $string ) {
Logger::fetch()->logPrint( Logger::ERROR, $string );
}
2017-10-21 02:54:28 +08:00
function Fatal( $string ) {
Logger::fetch()->logPrint( Logger::FATAL, $string );
if (Logger::fetch()->debugOn()) {
echo(htmlentities($string));
}
exit(1);
}
2017-10-21 02:54:28 +08:00
function Panic( $string ) {
if ( true ) {
// Use builtin function
ob_start();
debug_print_backtrace();
$backtrace = "\n".ob_get_clean();
} else {
// Roll your own
$backtrace = '';
$frames = debug_backtrace();
for ( $i = 0; $i < count($frames); $i++ ) {
$frame = $frames[$i];
$backtrace .= sprintf( "\n#%d %s() at %s/%d", $i, $frame['function'], $frame['file'], $frame['line'] );
}
2017-10-21 02:54:28 +08:00
}
Logger::fetch()->logPrint( Logger::PANIC, $string.$backtrace );
if (Logger::fetch()->debugOn()) {
echo $string;
}
exit(1);
}
2017-10-21 02:54:28 +08:00
function ErrorHandler( $error, $string, $file, $line ) {
if ( ! (error_reporting() & $error) ) {
// This error code is not included in error_reporting
return false;
2017-10-21 02:54:28 +08:00
}
switch ( $error ) {
case E_USER_ERROR:
Logger::fetch()->logPrint( Logger::FATAL, $string, $file, $line );
break;
case E_USER_WARNING:
Logger::fetch()->logPrint( Logger::ERROR, $string, $file, $line );
break;
case E_USER_NOTICE:
Logger::fetch()->logPrint( Logger::WARNING, $string, $file, $line );
break;
default:
Panic( "Unknown error type: [$error] $string" );
break;
}
return true;
}
?>