From a682ab9c0e211167984b76790fb1458aae7ead7c Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 19 Aug 2009 03:55:23 +0000 Subject: [PATCH] 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 --- dev/Debug.php | 156 ++++++++++++------------------- dev/SSBacktrace.php | 132 ++++++++++++++++++++++++++ dev/SSLog.php | 103 ++++++++++++++++++++ dev/SSLogEmailWriter.php | 48 ++++++++++ dev/SSLogErrorEmailFormatter.php | 52 +++++++++++ dev/SSZendLog.php | 34 +++++++ tests/dev/SSErrorLogTest.php | 29 ++++++ 7 files changed, 458 insertions(+), 96 deletions(-) create mode 100644 dev/SSBacktrace.php create mode 100644 dev/SSLog.php create mode 100644 dev/SSLogEmailWriter.php create mode 100644 dev/SSLogErrorEmailFormatter.php create mode 100644 dev/SSZendLog.php create mode 100644 tests/dev/SSErrorLogTest.php diff --git a/dev/Debug.php b/dev/Debug.php index 78f859c03..4151bfb75 100644 --- a/dev/Debug.php +++ b/dev/Debug.php @@ -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: + * + * $emailWriter = new SSLogEmailWriter('my@email.com'); + * SSLog::add_writer($emailWriter, SSLog::ERR); + * + * * @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 = "
\n"; - $data .= "

$errorType: $errstr
At line $errline in $errfile\n
\n
\n

\n"; - - $data .= Debug::backtrace(true); - $data .= "
\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 = ""; - 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); } /** diff --git a/dev/SSBacktrace.php b/dev/SSBacktrace.php new file mode 100644 index 000000000..d850b41a5 --- /dev/null +++ b/dev/SSBacktrace.php @@ -0,0 +1,132 @@ +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 = ""; + return $result; + } + +} \ No newline at end of file diff --git a/dev/SSLog.php b/dev/SSLog.php new file mode 100644 index 000000000..a120bdd19 --- /dev/null +++ b/dev/SSLog.php @@ -0,0 +1,103 @@ + + * $emailWriter = new SSErrorEmailWriter('my@email.com'); + * SSLog::add_writer($emailWriter, SSLog::ERR); + * + * + * @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 + } + } + +} \ No newline at end of file diff --git a/dev/SSLogEmailWriter.php b/dev/SSLogEmailWriter.php new file mode 100644 index 000000000..999f17dea --- /dev/null +++ b/dev/SSLogEmailWriter.php @@ -0,0 +1,48 @@ +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); + } + +} \ No newline at end of file diff --git a/dev/SSLogErrorEmailFormatter.php b/dev/SSLogErrorEmailFormatter.php new file mode 100644 index 000000000..7837eeda1 --- /dev/null +++ b/dev/SSLogErrorEmailFormatter.php @@ -0,0 +1,52 @@ +\n"; + $data .= "

$errorType: $errstr
At line $errline in $errfile\n
\n
\n

\n"; + + // Get a backtrace, filtering out debug method calls + $data .= SSBacktrace::backtrace(true, false, array( + 'SSLogErrorEmailFormatter->format', + 'SSLogEmailWriter->_write' + )); + + $data .= "\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 + ); + } + +} \ No newline at end of file diff --git a/dev/SSZendLog.php b/dev/SSZendLog.php new file mode 100644 index 000000000..325ab9309 --- /dev/null +++ b/dev/SSZendLog.php @@ -0,0 +1,34 @@ +_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]); + } + } + } + +} \ No newline at end of file diff --git a/tests/dev/SSErrorLogTest.php b/tests/dev/SSErrorLogTest.php new file mode 100644 index 000000000..d8b438f7c --- /dev/null +++ b/tests/dev/SSErrorLogTest.php @@ -0,0 +1,29 @@ +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)); + } + +} \ No newline at end of file