<?php
/**
* @see set_error_handler
* @author Cory Marsh
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
*/
/**
* take standard PHP errors and redirect them to the Rogue Log
* you can disable by setting the GLOBAL['NoLoggerHandler'] or by
* calling Logger::enableErrorHandler(false);
*/
if (!isset($GLOBALS['NoLoggerHandler']) || $GLOBALS['NoLoggerHandler'] !== true)
{
function NullHandler($errno, $errstr, $errfile, $errline)
{
return true;
}
function LoggerHandler($errno, $errstr, $errfile, $errline)
{
// if there is a problem with the Logger, we dont want to create a recursive loop here
set_error_handler('NullHandler');
$message = "$errstr on $errfile:$errline";
switch ($errno)
{
case E_USER_ERROR:
case E_CORE_ERROR:
case E_PARSE:
case E_COMPILE_WARNING:
case E_USER_ERROR:
default:
Logger::getLogger('phperror')->error($message);
break;
case E_WARNING:
case E_CORE_WARNING:
case E_RECOVERABLE_ERROR:
case E_USER_WARNING:
case E_NOTICE:
case E_USER_NOTICE:
Logger::getLogger('phperror')->warn($message);
}
return true;
}
set_error_handler('LoggerHandler');
}
// default the timezone to Boise (Rouge PHP's home!)
$tz = date_default_timezone_get();
if (!$tz)
date_default_timezone_set('America/Boise');
/**
* logger, similar to Log4J.
* <code>
* $log = Logger::getLogger('logName');
* $log->debug('This is a debug message');
* </code>
* @author Cory
* @version 2.0
*/
class Logger
{
// the log handle, and name
protected $_logName;
// min error level to save
protected $_logLevel;
// the format strings forthe log levels
protected $_formatStr;
// bool, true saves logs only at script exit
protected $_volatile;
// the syslog server to log to
protected static $_logHost;
// log socket to send syslog data to
protected static $_socket;
// log file to log to
protected static $_logFile;
// if the logs have been dumped yet
protected static $_written = false;
// all loggers
protected static $_loggers = array();
// all log messages for this logger
protected static $_entries = array();
// logger creation time
protected static $_creationTime = null;
// log levels
const DISABLE = -1;
const TRACE = 0;
const DEBUG = 1;
const INFO = 2;
const WARN = 3;
const ERROR = 4;
const FATAL = 5;
const SECURITY = 6;
const NONE = 98;
const VOLATILE = 99;
// syslog facilitys
const SYSLOG_USER = 1;
const SYSLOG_AUTH = 10;
const SYSLOG_LOCAL0 = 16;
const SYSLOG_LOCAL1 = 17;
const SYSLOG_LOCAL2 = 18;
const SYSLOG_LOCAL3 = 19;
const SYSLOG_LOCAL4 = 20;
const SYSLOG_LOCAL5 = 21;
const SYSLOG_LOCAL6 = 22;
const SYSLOG_LOCAL7 = 23;
// syslog levels
const SYSLOG_EMERG = 0;
const SYSLOG_ALRET = 1;
const SYSLOG_CRITICAL = 2;
const SYSLOG_ERROR = 3;
const SYSLOG_WARN = 4;
const SYSLOG_NOTICE = 5;
const SYSLOG_INFO = 6;
const SYSLOG_DEBUG = 7;
// the syslog level and facility
protected $_facility = Logger::SYSLOG_LOCAL0;
protected $_severity = Logger::SYSLOG_NOTICE;
/**
* volatile loggers only write to disk AFTER script completion. This keeps disk IO to a minimum.
* Suggested you override this default behavior with GLOBALS['LOGNONVOLATILE'] in development.
*
* @param string $name the unique logger name
* @param boolean $volatile
*/
private function __construct($name = false, $volatile = false)
{
$name = strtolower($name);
$this->_logName = $name;
$this->_formatStr = array();
// store the time the first logger is created
if (self::$_creationTime == null)
self::$_creationTime = microtime(true);
// set the minimum log level for this logger
if (isset($GLOBALS['LOGLEVEL-' . $name]))
$this->_logLevel = $GLOBALS['LOGLEVEL-' . $name];
else if (isset($GLOBALS['LOGLEVEL']))
$this->_logLevel = $GLOBALS['LOGLEVEL'];
else
$this->_logLevel = Logger::FATAL;
// override volatile setting with global override
$this->_volatile = $volatile;
if (isset($GLOBALS['LOGNONVOLATILE']) && $GLOBALS['LOGNONVOLATILE'])
$this->_volatile = false;
if (!$this->_volatile)
{
$this->connectStorage();
}
// select the correct log format for this named logger, or use the default
if (isset($GLOBALS['LOGFORMAT-' . $name]))
$format = $GLOBALS['LOGFORMAT-' . $name];
else if (isset($GLOBALS['LOGFORMAT']))
$format = $GLOBALS['LOGFORMAT'];
else
$format = '%v[%w]: [%I:%r:%n:%p]: %m';
// setup the log formats for each level
$this->_formatStr[Logger::TRACE] = $format;
$this->_formatStr[Logger::DEBUG] = $format;
$this->_formatStr[Logger::INFO] = $format;
$this->_formatStr[Logger::WARN] = $format;
$this->_formatStr[Logger::ERROR] = $format;
$this->_formatStr[Logger::FATAL] = $format;
if (isset($GLOBALS['LOGFORMAT:TRACE']))
$this->_formatStr[Logger::TRACE] = $GLOBALS['LOGFORMAT:TRACE'];
if (isset($GLOBALS['LOGFORMAT:DEBUG']))
$this->_formatStr[Logger::DEBUG] = $GLOBALS['LOGFORMAT:DEBUG'];
if (isset($GLOBALS['LOGFORMAT:INFO']))
$this->_formatStr[Logger::INFO] = $GLOBALS['LOGFORMAT:INFO'];
if (isset($GLOBALS['LOGFORMAT:WARN']))
$this->_formatStr[Logger::WARN] = $GLOBALS['LOGFORMAT:WARN'];
if (isset($GLOBALS['LOGFORMAT:ERROR']))
$this->_formatStr[Logger::ERROR] = $GLOBALS['LOGFORMAT:ERROR'];
if (isset($GLOBALS['LOGFORMAT:FATAL']))
$this->_formatStr[Logger::FATAL] = $GLOBALS['LOGFORMAT:FATAL'];
}
/**
* write the log data to disk on scipt end
**/
public function __destruct()
{
if (function_exists('NullHandler'))
set_error_handler('NullHandler');
// save the log to persitant storage
if ($this->_volatile && !$this->_written)
{
//echo "<pre>connect\n</pre>";
$this->connectStorage();
//echo "<pre>write\n</pre>";
// loop over each
do
{
// get the next message
$message = array_shift(Logger::$_entries);
if ($message == null || $message[1] < $GLOBALS['LOGLEVEL'])
continue;
// write to disk
if (Logger::$_logFile)
fwrite(Logger::$_logFile, $message[2] . "\n");
// write to syslog
if (Logger::$_socket)
{
$data = $this->syslogize(substr($message[2], 0, 990));
socket_sendto(Logger::$_socket, $data, strlen($data), 0, $GLOBALS['LOGHOST'], 514);
}
}
while ($message != null);
//echo "<pre>wrote\n</pre>";
// close remote connections
if (isset(Logger::$_socket) && Logger::$_socket != false)
socket_close(Logger::$_socket);
//echo "<pre>fin\n</pre>";
// sometimes we can hit the descructor twice (if writing the log creates a warning)
Logger::$_socket = false;
Logger::$_logFile = false;
$this->_written = true;
}
}
/**
* connect to persistant storage for saving log messages.
* connects to the logfile, and the remote syslog udp server IF they are
* configured.
* MUST call this method before writing to disk as $_logFile or sending
* data to $_socket;
* @since 1.2
*/
private function connectStorage()
{
// open the log file
if (!isset (Logger::$_logFile) && isset($GLOBALS['LOGFILE']))
Logger::$_logFile = fopen($GLOBALS['LOGFILE'], 'a+');
// open the syslogger
if (!isset (Logger::$_socket) && isset($GLOBALS['LOGHOST']))
Logger::$_socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
}
/**
* static singleton constructor. Always create a rootLogger if one does not exist yet
* @param string $name the logger name (similar to log facility, will be added to each log message
* @param boolean $volatile true if this is a volaitle logger
* (write's log as script end, not when messages arrive)
* @return Logger the new logger, ready for logging.
*/
public static function getLogger($name = false, $volatile = true)
{
// return an already constructed logger of this name (singleton-ish)
if (isset(Logger::$_loggers[$name]))
return Logger::$_loggers[$name];
return Logger::$_loggers[$name] = new Logger($name, $volatile);
}
/**
* get an HTML formatted version of the log. Gives you a div#logger.
* styles in bodyspace/styles.css can pretty print the HTML.
* @return string hidden HTML display of the log
* @since 1.2
*/
public static function getWebLog()
{
echo "WEBLOG: \n";
print_r($this);
die();
$output = '<div id="logger">';
foreach (Logger::$_entries as $entry)
{
$entry[2] = str_replace("\n", '<br/>', $entry[2]);
if ($entry[1] === Logger::ERROR || $entry[1] === Logger::FATAL)
$output .= "<br><span class='red'>$entry[2]</span>\n";
else if ($entry[1] == Logger::WARN)
$output .= "<br><span class='yellow'>$entry[2]</span>\n";
else if ($entry[1] == Logger::INFO)
$output .= "<br><span class='green'>$entry[2]</span>\n";
else if ($entry[1] == Logger::DEBUG)
$output .= "<br><span class='debug'>$entry[2]</span>\n";
else if ($entry[1] == Logger::TRACE)
$output .= "<br><span class='light'>$entry[2]</span>\n";
else
$output .= "<br>$entry[2]\n";
}
if ($diagnostics)
foreach ($diagnostics as $key => $value)
$output .= "<br><span style='color: blue'>$key in: $value secs</span>\n";
$output .= '</div>';
return $output;
}
/**
* set the minimum log level for the logger to actually log.
* available log levels are Logger::FATAL, ERROR, WARN, INFO, DEBUG, TRACE
* $log->_setLevel(Logger::$ERROR);
* $log->_warn('this will not log');
* $log->_error('this will log');
* @param integer $level one of the const logging levels
* @see Logger logger const settings
*/
public function setLevel($level = false)
{
$this->_logLevel = $level;
}
/**
* set the logging format. The default is: {%d [%n:%p] %m}
* unless the $GLOBAL variable LOGFORMAT is set, then that will be used instead
* setting this method will override the LOGFORMAT, but this can be overriden
* with the FORCELOGFORMAT $GLOBAL.
*
*
* Substitute symbol
* %d{dd/MM/yy HH:MM:ss } Date
* %t{HH:MM:ss } Time
* %w the logger instance id (sesison tracking)
* %r Milliseconds since logger was created
* %n logger name
* %p Level
* %m user-defined message
*
* %S Server Name
* %s PHP script name
* %I IP address of browser
* %H the request url
* %U HTTP user agent
* %Ccookie_name COOKIE content
* %u mybb2 cookie user slug
* %i mybb2 cookie user id
*
* %f File name
* %F complete File path
* %L Line number
* %M Method name
* %A Method arguments
* %B a formatted backtrace
* Caution: %f, %F, %L, %M, %A are slow formats
* @param string $format the format stringto set forthe logger
* @param integer $level the logging level to set the format for
* @return nothing
*/
public function setFormat($format = '%d [%n:%p] %m', $level = false)
{
if ($level == false)
{
$this->_formatStr[Logger::TRACE] = $format;
$this->_formatStr[Logger::DEBUG] = $format;
$this->_formatStr[Logger::INFO] = $format;
$this->_formatStr[Logger::WARN] = $format;
$this->_formatStr[Logger::ERROR] = $format;
$this->_formatStr[Logger::FATAL] = $format;
}
else
$this->_formatStr[$level] = $format;
}
/**
* return the current logging level
* @return integer the logging level as an integer
*/
public function getLevel()
{
return $this->_logLevel;
}
/**
* dump the error log to STDOUT for THIS logger. The root logger
* (Logger::getLogger('rootLogger')) will dump all errors
*/
public function dumpLog($dumpLevel = false)
{
$this->fatal('depricated method "dumpLog" called');
}
/**
* return the actual logging entries in the format:
* array (facility, level, message);
* @return array the log entries. each entry is of the form:
* array((string)logName, (int)logLevel, (string)message)
*/
public function getEntries()
{
return Logger::$_entries;
}
/**
* add a log line of value TRACE to the log journal
* @param string $message the message to log
* @return false;
*/
public function trace($message)
{
// don't log if the log level is too low
if ($this->_logLevel > Logger::INFO)
return false;
// format the mesage
$message = $this->msgFormat($message, 'trace', Logger::TRACE);
// add the log line to our entries
Logger::$_entries[] = array($this->_logName, (int)LOGGER::TRACE, $message);
$this->dispatch(LOGGER::TRACE, $message);
return false;
}
/**
* add a log line of value DEBUG to the log journal
* @param string $message the message to log
* @return false;
*/
public function debug($message)
{
// don't log if the log level is too low
if ($this->_logLevel > Logger::DEBUG)
return false;
// format the mesage
$message = $this->msgFormat($message, 'debug', Logger::DEBUG);
// add the log line to our entries
Logger::$_entries[] = array($this->_logName, LOGGER::DEBUG, $message);
$this->dispatch(LOGGER::DEBUG, $message);
return false;
}
/**
* add a log line of value INFO to the log journal
* @param string $message the message to log
* @return false;
*/
public function info($message)
{
// don't log if the log level is too low
if ($this->_logLevel > Logger::INFO)
return false;
// format the mesage
$message = $this->msgFormat($message, 'info', Logger::INFO);
// add the log line to our entries
Logger::$_entries[] = array($this->_logName, LOGGER::INFO, $message);
$this->dispatch(LOGGER::INFO, $message);
return false;
}
/**
* add a log line of value WARN to the log journal
* @param string $message the message to log
* @return false;
*/
public function warn($message)
{
// don't log if the log level is too low
if ($this->_logLevel > Logger::WARN)
return false;
// format the mesage
$message = $this->msgFormat($message, 'warn', Logger::WARN);
// add the log line to our entries
Logger::$_entries[] = array($this->_logName, LOGGER::WARN, $message);
$this->dispatch(LOGGER::WARN, $message);
return false;
}
/**
* add a log line of value ERROR to the log journal
* @param string $message the message to log
* @return false;
*/
public function error($message)
{
// don't log if the log level is too low
if ($this->_logLevel > Logger::ERROR)
return false;
// format the mesage
$message = $this->msgFormat($message, 'error', Logger::ERROR);
// add the log line to our entries
Logger::$_entries[] = array($this->_logName, LOGGER::ERROR, $message);
$this->dispatch(LOGGER::ERROR, $message);
return false;
}
/**
* add a log line of value FATAL to the log journal
* @param string $message the message to log
* @return false;
*/
public function fatal($message)
{
// don't log if the log level is too low
if ($this->_logLevel > Logger::FATAL)
return false;
// format the mesage
$message = $this->msgFormat($message, 'fatal', Logger::FATAL);
// add the log line to our entries
Logger::$_entries[] = array($this->_logName, LOGGER::FATAL, $message);
$this->dispatch(LOGGER::FATAL, $message);
return false;
}
/**
* add a log line of value SECURITY to the log journal
* @param string $message the message to log
* @return false;
*/
public function security($message)
{
// don't log if the log level is too low
if ($this->_logLevel > Logger::SECURITY)
return false;
// format the mesage
$message = $this->msgFormat($message, 'security', Logger::SECURITY);
// add the log line to our entries
Logger::$_entries[] = array($this->_logName, LOGGER::SECURITY, $message);
$this->dispatch(LOGGER::SECURITY, $message);
return false;
}
/**
* send a message to disk, and/or the syslog.
*
* @param integer $level the logging level for the message
* @param string $message the messsage to log
* @return nothing
*/
private function dispatch($level, $message)
{
// dont' write out volitile logger data
if ($this->_volatile)
return;
set_error_handler('NullHandler');
// send log data to the file
if (isset(Logger::$_logFile))
{
fwrite(Logger::$_logFile, $message . "\n");;
}
// send log data to syslog
if (Logger::$_socket)
{
$message = $this->syslogize(substr($message, 0, 990));
socket_sendto(Logger::$_socket, $message, strlen($message), 0, Logger::$_logHost, 514);
}
set_error_handler('LoggerHandler');
}
/**
* convert a normal message to a syslog message
* @param string $message the message to convert to syslog format
* @return string a syslog message with a syslog header for the current
* facility and severity level
*/
private function syslogize($message)
{
// Facility/severity
$header = '<' . ($this->_facility * 8 + $this->_severity) . '>' . date('M i G:i:s ');
// Host
if (isset($_SERVER['SERVER_NAME'])) {
$header .= $_SERVER['SERVER_NAME'] . ' ';
} elseif (function_exists('gethostname')) {
$header .= gethostname() . ' ';
} else {
$header .= 'unknown ';
}
return $header . $message;
}
/**
Substitute symbol
%d{dd/MM/yy HH:MM:ss } Date
%t{HH:MM:ss } Time
%w the logger instance id (sesison tracking)
%r Milliseconds since logger was created
%n logger name
%p Level
%m user-defined message
%S Server Name
%s PHP script name
%I IP address of browser
%H the request url
%U HTTP user agent
%Ccookie_name COOKIE content
%u mybb2 cookie user slug
%i mybb2 cookie user id
%f File name
%F complete File path
%L Line number
%M Method name
%A Method arguments
%B a formatted backtrace
%% individual percentage sign
Caution: %f, %F, %L, %M, %A slow down program run!
*/
private function msgFormat($message, $level, $levelNum = false)
{
// get the log format to use, and break it up into % tokens
if (isset($GLOBALS['FORCELOGFORMAT']))
$token = strtok($GLOBALS['FORCELOGFORMAT'], '%');
else if ($levelNum != false)
$token = strtok($this->_formatStr[$levelNum], '%');
else
$token = strtok($this->_formatStr[Logger::TRACE], '%');
$line = '';
$bt = false;
// loop over all tokens
while ($token !== false)
{
// the first char in the token is the token type (this is right after the %)
$type = $token[0];
switch ($type)
{
case 'd':
$line .= date('Y/m/d H:i:s');
break;
case 't':
$line .= date('H:i:s');
break;
case 'r':
$line .= round ((microtime(true) - self::$_creationTime), 6);
break;
case 'w':
$line .= getmypid();
break;
case 'p':
$line .= $level;
break;
case 'n':
$line .= substr($this->_logName, 0, 11);
break;
case 'm':
$line .= $message;
break;
case 'S':
if (isset($_SERVER['SERVER_NAME']))
$line .= $_SERVER['SERVER_NAME'];
break;
case 's':
if (isset($_SERVER['SCRIPT_FILENAME']))
$line .= $_SERVER['SCRIPT_FILENAME'];
break;
case 'U':
if (isset($_SERVER['HTTP_USER_AGENT']))
$line .= $_SERVER['HTTP_USER_AGENT'];
break;
case 'I':
if (isset($_SERVER['REMOTE_ADDR']))
$line .= $_SERVER['REMOTE_ADDR'];
else
$line .= 'unknownaddr';
break;
case 'H':
if (isset($_SERVER['REQUEST_URI']))
$line .= $_SERVER['REQUEST_URI'];
break;
case 'C':
if (isset($_COOKIE[substr($token, 1)]))
$line .= $_COOKIE[substr($token, 1)];
else
$line .= 'na';
break;
case 'u':
break;
case 'i':
if (isset($_COOKIE['mybb2']))
{
$parts = explode(':', $_COOKIE['mybb2']);
$line .= $parts[0];
}
else
$line .= 'na';
break;
case 'f':
if ($bt === false)
$bt = $this->getBtCaller();
$paths = explode('/', $bt['file']);
$line .= array_pop($paths);
break;
case 'F':
if ($bt === false)
$bt = $this->getBtCaller();
$line .= $bt['file'];
break;
case 'L':
case 'M':
if ($bt === false)
$bt = $this->getBtCaller();
$line .= $bt['line'];
break;
case 'A':
$bt = debug_backtrace();
$line .= var_export($bt[2]['args'], true);
break;
case 'B':
$line .= $this->formatBT();
break;
case 'v':
if (isset($_SERVER['HTTP_HOST'])) {
$line .= $_SERVER['HTTP_HOST'];
} else {
$line .= 'unknownhost';
}
break;
default:
$line .= $type;
break;
}
// append anything that is not a symbol as literal text to the string
if (isset($token[1]))
$line .= substr($token, 1);
// get the next token
$token = strtok('%');
}
// return the formatted string
return $line;
}
/**
* get the caller information through a backtrace
* @return string the backtrace caller that called the log message (2 deep)
*/
private static function getBtCaller()
{
$bt = debug_backtrace();
return $bt[2];
}
/**
* format a backtrace and return the result as a string. Each function call is
* displayed on a newline.
* @return string a stack trace, each stack level is separated by a newline.
*/
private function formatBT()
{
// get the backtrace
$bt = debug_backtrace();
$log = '';
// loop over the back trace elements EXCEPT for the call to formatBT()
for($i = 2, $m = count($bt); $i < $m; $i++)
{
$log .= "\t";
// the fine, lineno, function
$line = $bt[$i];
if(isset($line['file']))
$log .= $line['file'];
if(isset($line['line'] ))
$log .= ' line:[' . $line['line'] . '] ';
if(isset($line['function'] ))
$log .= ' func: ' . $line['function'];
$log .= "\n";
}
// return the list of function calls
return $log;
}
/**
* enable or disable the global error handler. This handler puts the PHP Errors into
* the Rogue Log.
* @param boolean $enable true to enable error handling (Default) or false to disable
*/
public static function enableErrorHandler($enable = true)
{
$GLOBALS['NoLoggerHandler'] = $enable;
}
}
?>