API CHANGE Moved Debug::backtrace() to SSBacktrace::backtrace()

API CHANGE Moved Debug::get_rendered_backtrace() to SSBacktrace::get_rendered_backtrace()
ENHANCEMENT Added SSLog, SSLogEmailWriter and SSLogErrorEmailFormatter for silverstripe message reporting
API CHANGE Debug::send_errors_to() and Debug::send_warnings_to() are deprecated in favour of SSLog. See class documentation for SSLog on configuration of error email notifications
MINOR Added SSLogTest for basic testing of the SSLog and SSLogEmailWriter classes



git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@84774 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Sean Harvey 2009-08-19 03:55:23 +00:00
parent 26ff1f9264
commit a682ab9c0e
7 changed files with 458 additions and 96 deletions

View File

@ -216,8 +216,21 @@ class Debug {
* @param unknown_type $errcontext
*/
static function warningHandler($errno, $errstr, $errfile, $errline, $errcontext) {
if(error_reporting() == 0) return;
if(error_reporting() == 0) return;
if(self::$send_warnings_to) self::emailError(self::$send_warnings_to, $errno, $errstr, $errfile, $errline, $errcontext, "Warning");
// Send out the error details to the logger for writing
SSLog::log(
array(
'errno' => $errno,
'errstr' => $errstr,
'errfile' => $errfile,
'errline' => $errline,
'errcontext' => $errcontext
),
SSLog::WARN
);
self::log_error_if_necessary( $errno, $errstr, $errfile, $errline, $errcontext, "Warning");
if(Director::isDev()) {
@ -237,12 +250,24 @@ class Debug {
* @param unknown_type $errcontext
*/
static function fatalHandler($errno, $errstr, $errfile, $errline, $errcontext) {
if(self::$send_errors_to) self::emailError(self::$send_errors_to, $errno, $errstr, $errfile, $errline, $errcontext, "Error");
// Send out the error details to the logger for writing
SSLog::log(
array(
'errno' => $errno,
'errstr' => $errstr,
'errfile' => $errfile,
'errline' => $errline,
'errcontext' => $errcontext
),
SSLog::ERR
);
self::log_error_if_necessary( $errno, $errstr, $errfile, $errline, $errcontext, "Error");
if(Director::isDev() || Director::is_cli()) {
Debug::showError($errno, $errstr, $errfile, $errline, $errcontext, "Error");
self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Error");
} else {
Debug::friendlyError();
@ -372,9 +397,14 @@ class Debug {
}
/**
* Dispatch an email notification message when an error is triggered.
* Uses the native PHP mail() function.
*
* Dispatch an email notification message when an error is triggered.
* @deprecated 2.5
* To create error logs by email, use this code instead:
* <code>
* $emailWriter = new SSLogEmailWriter('my@email.com');
* SSLog::add_writer($emailWriter, SSLog::ERR);
* </code>
*
* @param string $emailAddress
* @param string $errno
* @param string $errstr
@ -385,25 +415,22 @@ class Debug {
* @return boolean
*/
static function emailError($emailAddress, $errno, $errstr, $errfile, $errline, $errcontext, $errorType = "Error") {
if(strtolower($errorType) == 'warning') {
$colour = "orange";
} else {
$colour = "red";
}
$data = "<div style=\"border: 5px $colour solid\">\n";
$data .= "<p style=\"color: white; background-color: $colour; margin: 0\">$errorType: $errstr<br /> At line $errline in $errfile\n<br />\n<br />\n</p>\n";
$data .= Debug::backtrace(true);
$data .= "</div>\n";
// override smtp-server if needed
if(self::$custom_smtp_server) ini_set("SMTP", self::$custom_smtp_server);
$relfile = Director::makeRelative($errfile);
if($relfile[0] == '/') $relfile = substr($relfile,1);
return mail($emailAddress, "$errorType at $relfile line $errline (http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI])", $data, "Content-type: text/html\nFrom: errors@silverstripe.com");
user_error('Debug::send_errors_to() and Debug::emailError() is deprecated. Please use SSLog instead.
See the class documentation for SSLog for more information.', E_USER_NOTICE);
$priority = ($errorType == 'Error') ? SSLog::ERR : SSLog::WARN;
$writer = new SSLogEmailWriter($emailAddress);
SSLog::add_writer($writer, $priority);
SSLog::log(
array(
'errno' => $errno,
'errstr' => $errstr,
'errfile' => $errfile,
'errline' => $errline,
'errcontext' => $errcontext
),
$priority
);
SSLog::remove_writer($writer);
}
/**
@ -450,7 +477,7 @@ class Debug {
* Send errors to the given email address.
* Can be used like so:
* if(Director::isLive()) Debug::send_errors_to("sam@silverstripe.com");
*
*
* @param string $emailAddress The email address to send errors to
* @param string $sendWarnings Set to true to send warnings as well as errors (Default: false)
*/
@ -498,82 +525,19 @@ class Debug {
}
/**
* Render or return a backtrace from the given scope.
*
* @param unknown_type $returnVal
* @param unknown_type $ignoreAjax
* @return unknown
* @deprecated 2.5 Please use {@link SSBacktrace::backtrace()}
*/
static function backtrace($returnVal = false, $ignoreAjax = false) {
$bt = debug_backtrace();
$result = self::get_rendered_backtrace($bt, Director::is_cli() || (Director::is_ajax() && !$ignoreAjax));
if ($returnVal) {
return $result;
} else {
echo $result;
}
}
/**
* Render a backtrace array into an appropriate plain-text or HTML string.
* @param $bt The trace array, as returned by debug_backtrace() or Exception::getTrace().
* @param $plainText Set to false for HTML output, or true for plain-text output
*/
static function get_rendered_backtrace($bt, $plainText = false) {
// Ingore functions that are plumbing of the error handler
$ignoredFunctions = array('DebugView->writeTrace', 'CliDebugView->writeTrace',
'Debug::emailError','Debug::warningHandler','Debug::fatalHandler','errorHandler','Debug::showError',
'Debug::backtrace', 'exceptionHandler');
while( $bt && in_array(self::full_func_name($bt[0]), $ignoredFunctions) ) {
array_shift($bt);
}
$result = "<ul>";
foreach($bt as $item) {
if($plainText) {
$result .= self::full_func_name($item,true) . "\n";
if(isset($item['line']) && isset($item['file'])) $result .= "line $item[line] of " . basename($item['file']) . "\n";
$result .= "\n";
} else {
if ($item['function'] == 'user_error') {
$name = $item['args'][0];
} else {
$name = self::full_func_name($item,true);
}
$result .= "<li><b>" . htmlentities($name) . "</b>\n<br />\n";
$result .= isset($item['line']) ? "Line $item[line] of " : '';
$result .= isset($item['file']) ? htmlentities(basename($item['file'])) : '';
$result .= "</li>\n";
}
}
$result .= "</ul>";
return $result;
user_error('Debug::backtrace() is deprecated. Please use SSBacktrace::backtrace() instead', E_USER_NOTICE);
return SSBacktrace::backtrace($returnVal, $ignoreAjax);
}
/**
* Return the full function name. If showArgs is set to true, a string representation of the arguments will be shown
* @deprecated 2.5 Please use {@link SSBacktrace::get_rendered_backtrace()}
*/
static function full_func_name($item, $showArgs = false) {
$funcName = '';
if(isset($item['class'])) $funcName .= $item['class'];
if(isset($item['type'])) $funcName .= $item['type'];
if(isset($item['function'])) $funcName .= $item['function'];
if($showArgs && isset($item['args'])) {
$args = array();
foreach($item['args'] as $arg) {
if(!is_object($arg) || method_exists($arg, '__toString')) {
$args[] = (string) $arg;
} else {
$args[] = get_class($arg);
}
}
$funcName .= "(" . implode(",", $args) .")";
}
return $funcName;
static function get_rendered_backtrace($bt, $plainText = false) {
user_error('Debug::get_rendered_backtrace() is deprecated. Please use SSBacktrace::get_rendered_backtrace() instead', E_USER_NOTICE);
return SSBacktrace::get_rendered_backtrace($bt, $plainText);
}
/**

132
dev/SSBacktrace.php Normal file
View File

@ -0,0 +1,132 @@
<?php
/**
* @package sapphire
* @subpackage dev
*/
class SSBacktrace {
/**
* Return debug_backtrace() results with functions filtered
* specific to the debugging system, and not the trace.
*
* @param null|array $ignoredFunctions If an array, filter these functions out of the trace
* @return array
*/
static function filtered_backtrace($ignoredFunctions = null) {
return self::filter_backtrace(debug_backtrace(), $ignoredFunctions);
}
/**
* Filter a backtrace so that it doesn't show the calls to the
* debugging system, which is useless information.
*
* @param array $bt Backtrace to filter
* @param null|array $ignoredFunctions List of extra functions to filter out
* @return array
*/
static function filter_backtrace($bt, $ignoredFunctions = null) {
$defaultIgnoredFunctions = array(
'SSLog::log',
'SSBacktrace::backtrace',
'SSBacktrace::filtered_backtrace',
'Zend_Log_Writer_Abstract->write',
'Zend_Log->log',
'Zend_Log->__call',
'Zend_Log->err',
'DebugView->writeTrace',
'CliDebugView->writeTrace',
'Debug::emailError',
'Debug::warningHandler',
'Debug::fatalHandler',
'errorHandler',
'Debug::showError',
'Debug::backtrace',
'exceptionHandler'
);
if($ignoredFunctions) foreach($ignoredFunctions as $ignoredFunction) {
$defaultIgnoredFunctions[] = $ignoredFunction;
}
while($bt && in_array(self::full_func_name($bt[0]), $defaultIgnoredFunctions)) {
array_shift($bt);
}
return $bt;
}
/**
* Render or return a backtrace from the given scope.
*
* @param unknown_type $returnVal
* @param unknown_type $ignoreAjax
* @return unknown
*/
static function backtrace($returnVal = false, $ignoreAjax = false, $ignoredFunctions = null) {
$plainText = Director::is_cli() || (Director::is_ajax() && !$ignoreAjax);
$result = self::get_rendered_backtrace(debug_backtrace(), $plainText, $ignoredFunctions);
if($returnVal) {
return $result;
} else {
echo $result;
}
}
/**
* Return the full function name. If showArgs is set to true, a string representation of the arguments will be shown
*/
static function full_func_name($item, $showArgs = false) {
$funcName = '';
if(isset($item['class'])) $funcName .= $item['class'];
if(isset($item['type'])) $funcName .= $item['type'];
if(isset($item['function'])) $funcName .= $item['function'];
if($showArgs && isset($item['args'])) {
$args = array();
foreach($item['args'] as $arg) {
if(!is_object($arg) || method_exists($arg, '__toString')) {
$args[] = (string) $arg;
} else {
$args[] = get_class($arg);
}
}
$funcName .= "(" . implode(",", $args) .")";
}
return $funcName;
}
/**
* Render a backtrace array into an appropriate plain-text or HTML string.
*
* @param string $bt The trace array, as returned by debug_backtrace() or Exception::getTrace()
* @param boolean $plainText Set to false for HTML output, or true for plain-text output
* @param array List of functions that should be ignored. If not set, a default is provided
* @return string The rendered backtrace
*/
static function get_rendered_backtrace($bt, $plainText = false, $ignoredFunctions = null) {
$bt = self::filter_backtrace($bt, $ignoredFunctions);
$result = "<ul>";
foreach($bt as $item) {
if($plainText) {
$result .= self::full_func_name($item,true) . "\n";
if(isset($item['line']) && isset($item['file'])) $result .= "line $item[line] of " . basename($item['file']) . "\n";
$result .= "\n";
} else {
if ($item['function'] == 'user_error') {
$name = $item['args'][0];
} else {
$name = self::full_func_name($item,true);
}
$result .= "<li><b>" . htmlentities($name) . "</b>\n<br />\n";
$result .= isset($item['line']) ? "Line $item[line] of " : '';
$result .= isset($item['file']) ? htmlentities(basename($item['file'])) : '';
$result .= "</li>\n";
}
}
$result .= "</ul>";
return $result;
}
}

103
dev/SSLog.php Normal file
View File

@ -0,0 +1,103 @@
<?php
/**
* Wrapper class for a logging handler like {@link Zend_Log}
* which takes a message (or a map of context variables) and
* sends it to one or more {@link Zend_Log_Writer_Abstract}
* subclasses for output.
*
* The only priorities currently supported are SSLog::ERR and
* SSLog::WARN - this may change in the future if other types
* are to be supported.
*
* You can add an error writer by calling {@link SSLog::add_writer()}
*
* Example usage (called from mysite/_config.php):
* <code>
* $emailWriter = new SSErrorEmailWriter('my@email.com');
* SSLog::add_writer($emailWriter, SSLog::ERR);
* </code>
*
* @package sapphire
* @subpackage dev
*/
require_once 'Zend/Log.php';
class SSLog {
const ERR = Zend_Log::ERR;
const WARN = Zend_Log::WARN;
/**
* Logger class to use.
* @see SSLog::get_logger()
* @var string
*/
public static $logger_class = 'SSZendLog';
/**
* @see SSLog::get_logger()
* @var object
*/
protected static $logger;
/**
* Get the logger currently in use, or create a new
* one if it doesn't exist.
*
* @return object
*/
public static function get_logger() {
if(!self::$logger) {
self::$logger = new self::$logger_class;
}
return self::$logger;
}
/**
* Get all writers in use by the logger.
* @return array Collection of Zend_Log_Writer_Abstract instances
*/
public static function get_writers() {
return self::get_logger()->getWriters();
}
/**
* Remove a writer instance from the logger.
* @param object $writer Zend_Log_Writer_Abstract instance
*/
public static function remove_writer($writer) {
self::get_logger()->removeWriter($writer);
}
/**
* Add a writer instance to the logger.
* @param object $writer Zend_Log_Writer_Abstract instance
* @param const $priority Priority. Possible values: SSLog::ERR or SSLog::WARN
*/
public static function add_writer($writer, $priority = null) {
if($priority) $writer->addFilter(new Zend_Log_Filter_Priority($priority, '='));
self::get_logger()->addWriter($writer);
}
/**
* Dispatch a message by priority level.
*
* The message parameter can be either a string (a simple error
* message), or an array of variables. The latter is useful for passing
* along a list of debug information for the writer to handle, such as
* error code, error line, error context (backtrace).
*
* @param string|array $message String of error message, or array of variables
* @param const $priority Priority. Possible values: SSLog::ERR or SSLog::WARN
*/
public static function log($message, $priority) {
try {
self::get_logger()->log($message, $priority);
} catch(Exception $e) {
// @todo How do we handle exceptions thrown from Zend_Log?
// For example, an exception is thrown if no writers are added
}
}
}

48
dev/SSLogEmailWriter.php Normal file
View File

@ -0,0 +1,48 @@
<?php
/**
* Sends an error message to an email whenever an error occurs
* in sapphire.
*
* @package sapphire
* @subpackage dev
*/
require_once 'Zend/Log/Writer/Abstract.php';
class SSLogEmailWriter extends Zend_Log_Writer_Abstract {
protected $emailAddress;
protected $customSmtpServer;
public function __construct($emailAddress, $customSmtpServer = false) {
$this->emailAddress = $emailAddress;
$this->customSmtpServer = $customSmtpServer;
}
/**
* Send an email to the designated emails set in
* {@link Debug::send_errors_to()}
*/
public function _write($event) {
// If no formatter set up, use the default
if(!$this->_formatter) {
$formatter = new SSLogErrorEmailFormatter();
$this->setFormatter($formatter);
}
$formattedData = $this->_formatter->format($event);
$subject = $formattedData['subject'];
$data = $formattedData['data'];
$originalSMTP = ini_get('SMTP');
// override the SMTP server with a custom one if required
if($this->customSmtpServer) ini_set('SMTP', $this->customSmtpServer);
mail($this->emailAddress, $subject, $data, "Content-type: text/html\nFrom: errors@silverstripe.com");
// reset the SMTP server to the original
if($this->customSmtpServer) ini_set('SMTP', $originalSMTP);
}
}

View File

@ -0,0 +1,52 @@
<?php
/**
* Formats SS error emails with a basic layout.
* @package sapphire
* @subpackage dev
*/
require_once 'Zend/Log/Formatter/Interface.php';
class SSLogErrorEmailFormatter implements Zend_Log_Formatter_Interface {
public function format($event) {
$errorType = ($event['priorityName'] == 'WARN') ? 'Warning' : 'Error';
if($event['priorityName'] == 'WARN') {
$colour = "orange";
} else {
$colour = "red";
}
if(!is_array($event['message'])) {
return false;
}
$errno = $event['message']['errno'];
$errstr = $event['message']['errstr'];
$errfile = $event['message']['errfile'];
$errline = $event['message']['errline'];
$errcontext = $event['message']['errcontext'];
$data = "<div style=\"border: 5px $colour solid\">\n";
$data .= "<p style=\"color: white; background-color: $colour; margin: 0\">$errorType: $errstr<br /> At line $errline in $errfile\n<br />\n<br />\n</p>\n";
// Get a backtrace, filtering out debug method calls
$data .= SSBacktrace::backtrace(true, false, array(
'SSLogErrorEmailFormatter->format',
'SSLogEmailWriter->_write'
));
$data .= "</div>\n";
$relfile = Director::makeRelative($errfile);
if($relfile[0] == '/') $relfile = substr($relfile, 1);
$subject = "$errorType at $relfile line {$errline} (http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI])";
return array(
'subject' => $subject,
'data' => $data
);
}
}

34
dev/SSZendLog.php Normal file
View File

@ -0,0 +1,34 @@
<?php
/**
* Modifications to Zend_Log to make it work nicer
* with SSErrorLog. Specifically, this includes removing
* writers that have been added to the logger, as well as
* listing which ones are currently in use.
*
* @package sapphire
* @subpackage dev
*/
require_once 'Zend/Log.php';
class SSZendLog extends Zend_Log {
public function getWriters() {
return $this->_writers;
}
/**
* Remove a writer instance that exists in
* the current writers collection for this logger.
*
* @param object Zend_Log_Writer_Abstract instance
*/
public function removeWriter($writer) {
foreach($this->_writers as $index => $existingWriter) {
if($existingWriter == $writer) {
unset($this->_writers[$index]);
}
}
}
}

View File

@ -0,0 +1,29 @@
<?php
/**
* @package sapphire
* @subpackage tests
*/
class SSLogTest extends SapphireTest {
protected $testEmailWriter;
function setUp() {
parent::setUp();
$this->testEmailWriter = new SSLogEmailWriter('sean@silverstripe.com');
SSLog::add_writer($this->testEmailWriter, SSLog::ERR);
}
function testExistingWriter() {
$writers = SSLog::get_writers();
$this->assertType('array', $writers);
$this->assertEquals(1, count($writers));
}
function testRemoveWriter() {
SSLog::remove_writer($this->testEmailWriter);
$writers = SSLog::get_writers();
$this->assertType('array', $writers);
$this->assertEquals(0, count($writers));
}
}