From 1b8d2957673f50ec7c7e52bfb70980aa28519634 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Fri, 24 Jul 2015 10:53:41 +1200 Subject: [PATCH] API CHANGE: Shift to Monolog for error reporting and logging API CHANGE: Debug::showError(), Debug::showLines(), Debug::log(), and Debug::header() removed NEW: Logging provided MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ZendLog has been removed and monolog introduced instead as a dependency. The “ErrorLogger” injection point is now the used as the logger that errors are fed into, and implements PSR-3’s Psr\Log\LoggerInterface. The SS_ERROR_LOG setting expect a Monolog Logger to be provided as the ErrorLogger. --- _config/logging.yml | 45 +++ composer.json | 3 +- conf/ConfigureFromEnv.php | 10 +- control/HTTPResponse.php | 5 +- core/Core.php | 6 +- dev/Debug.php | 361 +----------------- dev/DebugView.php | 32 ++ dev/Log.php | 185 --------- dev/LogEmailWriter.php | 94 ----- dev/LogErrorEmailFormatter.php | 80 ---- dev/LogErrorFileFormatter.php | 43 --- dev/LogFileWriter.php | 65 ---- dev/SysLogWriter.php | 51 --- dev/ZendLog.php | 43 --- .../07_Debugging/01_Error_Handling.md | 244 ++++++++++-- i18n/i18n.php | 4 +- logging/DebugViewFriendlyErrorFormatter.php | 90 +++++ logging/DetailedErrorFormatter.php | 118 ++++++ logging/HTTPOutputHandler.php | 73 ++++ logging/Log.php | 89 +++++ logging/MonologErrorHandler.php | 30 ++ tests/dev/LogTest.php | 138 ------- 22 files changed, 713 insertions(+), 1096 deletions(-) create mode 100644 _config/logging.yml delete mode 100644 dev/Log.php delete mode 100644 dev/LogEmailWriter.php delete mode 100644 dev/LogErrorEmailFormatter.php delete mode 100644 dev/LogErrorFileFormatter.php delete mode 100644 dev/LogFileWriter.php delete mode 100644 dev/SysLogWriter.php delete mode 100644 dev/ZendLog.php create mode 100644 logging/DebugViewFriendlyErrorFormatter.php create mode 100644 logging/DetailedErrorFormatter.php create mode 100644 logging/HTTPOutputHandler.php create mode 100644 logging/Log.php create mode 100644 logging/MonologErrorHandler.php delete mode 100644 tests/dev/LogTest.php diff --git a/_config/logging.yml b/_config/logging.yml new file mode 100644 index 000000000..557d1de26 --- /dev/null +++ b/_config/logging.yml @@ -0,0 +1,45 @@ +--- +Name: logging +--- +Injector: + ErrorHandler: + class: SilverStripe\Framework\Logging\MonologErrorHandler + properties: + Logger: %$Logger + Logger: + type: singleton + class: Monolog\Logger + constructor: + - "error-log" + calls: + DisplayErrorHandler: [ pushHandler, [ %$DisplayErrorHandler ] ] + +--- +Name: dev-logging +Only: + environment: dev +--- +Injector: + DisplayErrorHandler: + class: SilverStripe\Framework\Logging\HTTPOutputHandler + constructor: + - "notice" + properties: + Formatter: %$SilverStripe\Framework\Logging\DetailedErrorFormatter +--- +Name: live-logging +Except: + environment: dev +--- +Injector: + DisplayErrorHandler: + class: SilverStripe\Framework\Logging\HTTPOutputHandler + constructor: + - "error" + properties: + Formatter: %$FriendlyErrorFormatter + FriendlyErrorFormatter: + class: SilverStripe\Framework\Logging\DebugViewFriendlyErrorFormatter + properties: + Title: "There has been an error" + Body: "The website server has not been able to respond to your request" diff --git a/composer.json b/composer.json index 83c28849d..55c4a1e07 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ ], "require": { "php": ">=5.4.0", - "composer/installers": "~1.0" + "composer/installers": "~1.0", + "monolog/monolog": "~1.11" }, "require-dev": { "phpunit/PHPUnit": "~3.7" diff --git a/conf/ConfigureFromEnv.php b/conf/ConfigureFromEnv.php index 2542150f0..2a6d44ba9 100644 --- a/conf/ConfigureFromEnv.php +++ b/conf/ConfigureFromEnv.php @@ -50,6 +50,9 @@ * @subpackage core */ +use Monolog\Logger; +use Monolog\Handler\StreamHandler; + /* * _ss_environment.php handler */ @@ -139,7 +142,12 @@ if(defined('SS_USE_BASIC_AUTH') && SS_USE_BASIC_AUTH) { } if(defined('SS_ERROR_LOG')) { - SS_Log::add_writer(new SS_LogFileWriter(BASE_PATH . '/' . SS_ERROR_LOG), SS_Log::WARN, '<='); + $logger = Injector::inst()->get('Logger'); + if($logger instanceof Logger) { + $logger->pushHandler(new StreamHandler(BASE_PATH . '/' . SS_ERROR_LOG, Logger::WARN)); + } else { + user_error("SS_ERROR_LOG setting only works with Monolog, you are using another logger", E_USER_WARNING); + } } // Allow database adapters to handle their own configuration diff --git a/control/HTTPResponse.php b/control/HTTPResponse.php index 40d1db497..14e04d0c9 100644 --- a/control/HTTPResponse.php +++ b/control/HTTPResponse.php @@ -271,7 +271,10 @@ EOT // an error, and the response doesn't have any body yet that might contain // a more specific error description. if(Director::isLive() && $this->isError() && !$this->body) { - Debug::friendlyError($this->statusCode, $this->getStatusDescription()); + $formatter = Injector::get('FriendlyErrorFormatter'); + $formatter->setStatusCode($this->statusCode); + echo $formatter->format(array()); + } else { echo $this->body; } diff --git a/core/Core.php b/core/Core.php index 756941a1c..05c8c7615 100644 --- a/core/Core.php +++ b/core/Core.php @@ -70,9 +70,8 @@ require_once 'view/TemplateGlobalProvider.php'; require_once 'control/Director.php'; require_once 'dev/Debug.php'; require_once 'dev/DebugView.php'; +require_once 'dev/CliDebugView.php'; require_once 'dev/Backtrace.php'; -require_once 'dev/ZendLog.php'; -require_once 'dev/Log.php'; require_once 'filesystem/FileFinder.php'; require_once 'core/manifest/ManifestCache.php'; require_once 'core/manifest/ClassLoader.php'; @@ -136,8 +135,9 @@ if(Director::isLive()) { /** * Load error handlers */ -Debug::loadErrorHandlers(); +$errorHandler = Injector::inst()->get('ErrorHandler'); +$errorHandler->start(); /////////////////////////////////////////////////////////////////////////////// // HELPER FUNCTIONS diff --git a/dev/Debug.php b/dev/Debug.php index 3c7480775..dae6ddf56 100644 --- a/dev/Debug.php +++ b/dev/Debug.php @@ -1,4 +1,5 @@ $errno, - 'errstr' => $errstr, - 'errfile' => $errfile, - 'errline' => $errline, - 'errcontext' => $errcontext - ), - SS_Log::NOTICE - ); - - if(Director::isDev()) { - return self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Notice"); - } else { - return false; - } - } - - /** - * Handle a non-fatal warning error thrown by PHP interpreter. - * - * @param unknown_type $errno - * @param unknown_type $errstr - * @param unknown_type $errfile - * @param unknown_type $errline - * @param unknown_type $errcontext - */ - public static function warningHandler($errno, $errstr, $errfile, $errline, $errcontext) { - if(error_reporting() == 0) return; - ini_set('display_errors', 0); - - // Send out the error details to the logger for writing - SS_Log::log( - array( - 'errno' => $errno, - 'errstr' => $errstr, - 'errfile' => $errfile, - 'errline' => $errline, - 'errcontext' => $errcontext - ), - SS_Log::WARN - ); - - if(Director::isDev()) { - return self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Warning"); - } else { - return false; - } - } - - /** - * Handle a fatal error, depending on the mode of the site (ie: Dev, Test, or Live). - * - * Runtime execution dies immediately once the error is generated. - * - * @param unknown_type $errno - * @param unknown_type $errstr - * @param unknown_type $errfile - * @param unknown_type $errline - * @param unknown_type $errcontext - */ - public static function fatalHandler($errno, $errstr, $errfile, $errline, $errcontext) { - ini_set('display_errors', 0); - - // Send out the error details to the logger for writing - SS_Log::log( - array( - 'errno' => $errno, - 'errstr' => $errstr, - 'errfile' => $errfile, - 'errline' => $errline, - 'errcontext' => $errcontext - ), - SS_Log::ERR - ); - - if(Director::isDev() || Director::is_cli()) { - self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Error"); - } else { - self::friendlyError(); - } - return false; - } - - /** - * Render a user-facing error page, using the default HTML error template - * rendered by {@link ErrorPage} if it exists. Doesn't use the standard {@link SS_HTTPResponse} class - * the keep dependencies minimal. - * - * @uses ErrorPage - * - * @param int $statusCode HTTP Status Code (Default: 500) - * @param string $friendlyErrorMessage User-focused error message. Should not contain code pointers - * or "tech-speak". Used in the HTTP Header and ajax responses. - * @param string $friendlyErrorDetail Detailed user-focused message. Is just used if no {@link ErrorPage} is found - * for this specific status code. - * @return string HTML error message for non-ajax requests, plaintext for ajax-request. - */ - public static function friendlyError($statusCode=500, $friendlyErrorMessage=null, $friendlyErrorDetail=null) { - if(!$friendlyErrorMessage) { - $friendlyErrorMessage = Config::inst()->get('Debug', 'friendly_error_header'); - } - - if(!$friendlyErrorDetail) { - $friendlyErrorDetail = Config::inst()->get('Debug', 'friendly_error_detail'); - } - - if(!headers_sent()) { - // Ensure the error message complies with the HTTP 1.1 spec - $msg = strip_tags(str_replace(array("\n", "\r"), '', $friendlyErrorMessage)); - if(Controller::has_curr()) { - $response = Controller::curr()->getResponse(); - $response->setStatusCode($statusCode, $msg); - } else { - header($_SERVER['SERVER_PROTOCOL'] . " $statusCode $msg"); - } - } - - if(Director::is_ajax()) { - echo $friendlyErrorMessage; - } else { - if(class_exists('ErrorPage')){ - $errorFilePath = ErrorPage::get_filepath_for_errorcode( - $statusCode, - class_exists('Translatable') ? Translatable::get_current_locale() : null - ); - if(file_exists($errorFilePath)) { - $content = file_get_contents($errorFilePath); - if(!headers_sent()) header('Content-Type: text/html'); - // $BaseURL is left dynamic in error-###.html, so that multi-domain sites don't get broken - echo str_replace('$BaseURL', Director::absoluteBaseURL(), $content); - } - } else { - $renderer = new DebugView(); - $renderer->writeHeader(); - $renderer->writeInfo("Website Error", $friendlyErrorMessage, $friendlyErrorDetail); - - if(Email::config()->admin_email) { - $mailto = Email::obfuscate(Email::config()->admin_email); - $renderer->writeParagraph('Contact an administrator: ' . $mailto . ''); - } - - $renderer->writeFooter(); - } - } - return false; - } - /** * Create an instance of an appropriate DebugView object. * @@ -379,84 +160,6 @@ class Debug { return Injector::inst()->get($service); } - /** - * Render a developer facing error page, showing the stack trace and details - * of the code where the error occured. - * - * @param unknown_type $errno - * @param unknown_type $errstr - * @param unknown_type $errfile - * @param unknown_type $errline - * @param unknown_type $errcontext - */ - public static function showError($errno, $errstr, $errfile, $errline, $errcontext, $errtype) { - if(!headers_sent()) { - $errText = "$errtype at line $errline of $errfile"; - $errText = str_replace(array("\n","\r")," ",$errText); - - if(!headers_sent()) header($_SERVER['SERVER_PROTOCOL'] . " 500 $errText"); - - // if error is displayed through ajax with CliDebugView, use plaintext output - if(Director::is_ajax()) { - header('Content-Type: text/plain'); - } - } - - $reporter = self::create_debug_view(); - - // Coupling alert: This relies on knowledge of how the director gets its URL, it could be improved. - $httpRequest = null; - if(isset($_SERVER['REQUEST_URI'])) { - $httpRequest = $_SERVER['REQUEST_URI']; - } elseif(isset($_REQUEST['url'])) { - $httpRequest = $_REQUEST['url']; - } - if(isset($_SERVER['REQUEST_METHOD'])) $httpRequest = $_SERVER['REQUEST_METHOD'] . ' ' . $httpRequest; - - $reporter->writeHeader($httpRequest); - $reporter->writeError($httpRequest, $errno, $errstr, $errfile, $errline, $errcontext); - - if(file_exists($errfile)) { - $lines = file($errfile); - - // Make the array 1-based - array_unshift($lines,""); - unset($lines[0]); - - $offset = $errline-10; - $lines = array_slice($lines, $offset, 16, true); - $reporter->writeSourceFragment($lines, $errline); - } - $reporter->writeTrace(($errcontext ? $errcontext : debug_backtrace())); - $reporter->writeFooter(); - return false; - } - - /** - * Utility method to render a snippet of PHP source code, from selected file - * and highlighting the given line number. - * - * @param string $errfile - * @param int $errline - */ - public static function showLines($errfile, $errline) { - $lines = file($errfile); - $offset = $errline-10; - $lines = array_slice($lines, $offset, 16); - echo '
';
-		$offset++;
-		foreach($lines as $line) {
-			$line = htmlentities($line, ENT_COMPAT, 'UTF-8');
-			if ($offset == $errline) {
-				echo "$offset $line";
-			} else {
-				echo "$offset $line";
-			}
-			$offset++;
-		}
-		echo '
'; - } - /** * Check if the user has permissions to run URL debug tools, * else redirect them to log in. @@ -501,65 +204,3 @@ class Debug { die(); } } - - - - - - - - - - -/** - * Generic callback, to catch uncaught exceptions when they bubble up to the top of the call chain. - * - * @ignore - * @param Exception $exception - */ -function exceptionHandler($exception) { - $errno = E_USER_ERROR; - $type = get_class($exception); - $message = "Uncaught " . $type . ": " . $exception->getMessage(); - $file = $exception->getFile(); - $line = $exception->getLine(); - $context = $exception->getTrace(); - Debug::fatalHandler($errno, $message, $file, $line, $context); - exit(1); -} - -/** - * Generic callback to catch standard PHP runtime errors thrown by the interpreter - * or manually triggered with the user_error function. Any unknown error codes are treated as - * fatal errors. - * Caution: The error levels default to E_ALL if the site is in dev-mode (set in main.php). - * - * @ignore - * @param int $errno - * @param string $errstr - * @param string $errfile - * @param int $errline - */ -function errorHandler($errno, $errstr, $errfile, $errline) { - switch($errno) { - case E_NOTICE: - case E_USER_NOTICE: - case E_DEPRECATED: - case E_USER_DEPRECATED: - case E_STRICT: - return Debug::noticeHandler($errno, $errstr, $errfile, $errline, debug_backtrace()); - - case E_WARNING: - case E_CORE_WARNING: - case E_USER_WARNING: - case E_RECOVERABLE_ERROR: - return Debug::warningHandler($errno, $errstr, $errfile, $errline, debug_backtrace()); - - case E_ERROR: - case E_CORE_ERROR: - case E_USER_ERROR: - default: - Debug::fatalHandler($errno, $errstr, $errfile, $errline, debug_backtrace()); - exit(1); - } -} diff --git a/dev/DebugView.php b/dev/DebugView.php index 5e73ed807..5a2c5e7c4 100644 --- a/dev/DebugView.php +++ b/dev/DebugView.php @@ -23,6 +23,38 @@ class DebugView extends Object private static $columns = 100; protected static $error_types = array( + 0 => array( + 'title' => 'Emergency', + 'class' => 'error' + ), + 1 => array( + 'title' => 'Alert', + 'class' => 'error' + ), + 2 => array( + 'title' => 'Critical', + 'class' => 'error' + ), + 3 => array( + 'title' => 'Error', + 'class' => 'error' + ), + 4 => array( + 'title' => 'Warning', + 'class' => 'warning' + ), + 5 => array( + 'title' => 'Notice', + 'class' => 'notice' + ), + 6 => array( + 'title' => 'Information', + 'class' => 'info' + ), + 7=> array( + 'title' => 'Debug', + 'class' => 'debug' + ), E_USER_ERROR => array( 'title' => 'User Error', 'class' => 'error' diff --git a/dev/Log.php b/dev/Log.php deleted file mode 100644 index fe0c0ecaa..000000000 --- a/dev/Log.php +++ /dev/null @@ -1,185 +0,0 @@ - - * SS_Log::add_writer(new SS_LogEmailWriter('my@email.com'), SS_Log::ERR); - * - * - * Example usage of logging errors by file: - * - * SS_Log::add_writer(new SS_LogFileWriter('/var/log/silverstripe/errors.log'), SS_Log::ERR); - * - * - * Example usage of logging at warnings and errors by setting the priority to '<=': - * - * SS_Log::add_writer(new SS_LogEmailWriter('my@email.com'), SS_Log::WARN, '<='); - * - * - * Each writer object can be assigned a formatter. The formatter is - * responsible for formatting the message before giving it to the writer. - * {@link SS_LogErrorEmailFormatter} is such an example that formats errors - * into HTML for human readability in an email client. - * - * Formatters are added to writers like this: - * - * $logEmailWriter = new SS_LogEmailWriter('my@email.com'); - * $myEmailFormatter = new MyLogEmailFormatter(); - * $logEmailWriter->setFormatter($myEmailFormatter); - * - * - * @package framework - * @subpackage dev - */ -class SS_Log { - - const ERR = Zend_Log::ERR; - const WARN = Zend_Log::WARN; - const NOTICE = Zend_Log::NOTICE; - const INFO = Zend_Log::INFO; - const DEBUG = Zend_Log::DEBUG; - - /** - * Logger class to use. - * @see SS_Log::get_logger() - * @var string - */ - public static $logger_class = 'SS_ZendLog'; - - /** - * @see SS_Log::get_logger() - * @var object - */ - protected static $logger; - - /** - * @var array Logs additional context from PHP's superglobals. - * Caution: Depends on logger implementation (mainly targeted at {@link SS_LogEmailWriter}). - * @see http://framework.zend.com/manual/en/zend.log.overview.html#zend.log.overview.understanding-fields - */ - protected static $log_globals = array( - '_SERVER' => array( - 'HTTP_ACCEPT', - 'HTTP_ACCEPT_CHARSET', - 'HTTP_ACCEPT_ENCODING', - 'HTTP_ACCEPT_LANGUAGE', - 'HTTP_REFERRER', - 'HTTP_USER_AGENT', - 'HTTPS', - 'REMOTE_ADDR', - ), - ); - - /** - * Get the logger currently in use, or create a new one if it doesn't exist. - * - * @return object - */ - public static function get_logger() { - if(!static::$logger) { - // Create default logger - static::$logger = new static::$logger_class; - - // Add default context (shouldn't change until the actual log event happens) - foreach(static::$log_globals as $globalName => $keys) { - foreach($keys as $key) { - $val = isset($GLOBALS[$globalName][$key]) ? $GLOBALS[$globalName][$key] : null; - static::$logger->setEventItem(sprintf('$%s[\'%s\']', $globalName, $key), $val); - } - } - - } - return static::$logger; - } - - /** - * Get all writers in use by the logger. - * @return array Collection of Zend_Log_Writer_Abstract instances - */ - public static function get_writers() { - return static::get_logger()->getWriters(); - } - - /** - * Remove all writers currently in use. - */ - public static function clear_writers() { - static::get_logger()->clearWriters(); - } - - /** - * Remove a writer instance from the logger. - * @param object $writer Zend_Log_Writer_Abstract instance - */ - public static function remove_writer($writer) { - static::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: SS_Log::ERR, SS_Log::WARN or SS_Log::NOTICE - * @param $comparison Priority comparison operator. Acts on the integer values of the error - * levels, where more serious errors are lower numbers. By default this is "=", which means only - * the given priority will be logged. Set to "<=" if you want to track errors of *at least* - * the given priority. - */ - public static function add_writer($writer, $priority = null, $comparison = '=') { - if($priority) $writer->addFilter(new Zend_Log_Filter_Priority($priority, $comparison)); - static::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 mixed $message Exception object or array of error context variables - * @param const $priority Priority. Possible values: SS_Log::ERR, SS_Log::WARN or SS_Log::NOTICE - * @param mixed $extras Extra information to log in event - */ - public static function log($message, $priority, $extras = null) { - if($message instanceof Exception) { - $message = array( - 'errno' => '', - 'errstr' => $message->getMessage(), - 'errfile' => $message->getFile(), - 'errline' => $message->getLine(), - 'errcontext' => $message->getTrace() - ); - } elseif(is_string($message)) { - $trace = SS_Backtrace::filtered_backtrace(); - $lastTrace = $trace[0]; - $message = array( - 'errno' => '', - 'errstr' => $message, - 'errfile' => isset($lastTrace['file']) ? $lastTrace['file'] : null, - 'errline' => isset($lastTrace['line']) ? $lastTrace['line'] : null, - 'errcontext' => $trace - ); - } - try { - static::get_logger()->log($message, $priority, $extras); - } catch(Exception $e) { - // @todo How do we handle exceptions thrown from Zend_Log? - // For example, an exception is thrown if no writers are added - } - } - -} diff --git a/dev/LogEmailWriter.php b/dev/LogEmailWriter.php deleted file mode 100644 index 6a4d81b17..000000000 --- a/dev/LogEmailWriter.php +++ /dev/null @@ -1,94 +0,0 @@ -emailAddress = $emailAddress; - $this->customSmtpServer = $customSmtpServer; - } - - public static function factory($emailAddress, $customSmtpServer = false) { - return new SS_LogEmailWriter($emailAddress, $customSmtpServer); - } - - /** - * @deprecated 4.0 Use the "SS_LogEmailWriter.send_from" config setting instead - */ - public static function set_send_from($address) { - Deprecation::notice('4.0', 'Use the "SS_LogEmailWriter.send_from" config setting instead'); - Config::inst()->update('SS_LogEmailWriter', 'send_from', $address); - } - - /** - * @deprecated 4.0 Use the "SS_LogEmailWriter.send_from" config setting instead - */ - public static function get_send_from() { - Deprecation::notice('4.0', 'Use the "SS_LogEmailWriter.send_from" config setting instead'); - return Config::inst()->get('SS_LogEmailWriter', 'send_from'); - } - - /** - * Send an email to the email address set in - * this writer. - */ - public function _write($event) { - // If no formatter set up, use the default - if(!$this->_formatter) { - $formatter = new SS_LogErrorEmailFormatter(); - $this->setFormatter($formatter); - } - - $formattedData = $this->_formatter->format($event); - $subject = $formattedData['subject']; - $data = $formattedData['data']; - $from = Config::inst()->get('SS_LogEmailWriter', 'send_from'); - - // override the SMTP server with a custom one if required - $originalSMTP = ini_get('SMTP'); - if($this->customSmtpServer) ini_set('SMTP', $this->customSmtpServer); - - // Use plain mail() implementation to avoid complexity of Mailer implementation. - // Only use built-in mailer when we're in test mode (to allow introspection) - $mailer = Email::mailer(); - if($mailer instanceof TestMailer) { - $mailer->sendHTML( - $this->emailAddress, - null, - $subject, - $data, - null, - "Content-type: text/html\nFrom: " . $from - ); - } else { - mail( - $this->emailAddress, - $subject, - $data, - "Content-type: text/html\nFrom: " . $from - ); - } - - // reset the SMTP server to the original - if($this->customSmtpServer) ini_set('SMTP', $originalSMTP); - } - -} diff --git a/dev/LogErrorEmailFormatter.php b/dev/LogErrorEmailFormatter.php deleted file mode 100644 index 299393e62..000000000 --- a/dev/LogErrorEmailFormatter.php +++ /dev/null @@ -1,80 +0,0 @@ -html, body, table {font-family: sans-serif; font-size: 12px;}'; - $data .= "
\n"; - $data .= "

[$errorType] "; - $data .= nl2br(htmlspecialchars($errstr))."
$errfile:$errline\n
\n
\n

\n"; - - // Render the provided backtrace - $data .= SS_Backtrace::get_rendered_backtrace($errcontext); - - // Compile extra data - $blacklist = array('message', 'timestamp', 'priority', 'priorityName'); - $extras = array_diff_key($event, array_combine($blacklist, $blacklist)); - if($extras) { - $data .= "

Details

\n"; - $data .= "\n"; - foreach($extras as $k => $v) { - if(is_array($v)) $v = var_export($v, true); - $data .= sprintf( - "\n", $k, $v); - } - $data .= "
%s
%s
\n"; - } - - $data .= "
\n"; - - $relfile = Director::makeRelative($errfile); - if($relfile && $relfile[0] == '/') $relfile = substr($relfile, 1); - - $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : null; - $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : null; - - $subject = "[$errorType] in $relfile:{$errline} (http://{$host}{$uri})"; - - return array( - 'subject' => $subject, - 'data' => $data - ); - } - -} diff --git a/dev/LogErrorFileFormatter.php b/dev/LogErrorFileFormatter.php deleted file mode 100644 index 5a64d8ca7..000000000 --- a/dev/LogErrorFileFormatter.php +++ /dev/null @@ -1,43 +0,0 @@ - at line : - * @package framework - * @subpackage dev - */ -class SS_LogErrorFileFormatter implements Zend_Log_Formatter_Interface { - - public function format($event) { - $errno = $event['message']['errno']; - $errstr = $event['message']['errstr']; - $errfile = $event['message']['errfile']; - $errline = $event['message']['errline']; - $errcontext = $event['message']['errcontext']; - - switch($event['priorityName']) { - case 'ERR': - $errtype = 'Error'; - break; - case 'WARN': - $errtype = 'Warning'; - break; - case 'NOTICE': - $errtype = 'Notice'; - break; - default: - $errtype = $event['priorityName']; - } - - $urlSuffix = ''; - $relfile = Director::makeRelative($errfile); - if(strlen($relfile) && $relfile[0] == '/') $relfile = substr($relfile, 1); - if(isset($_SERVER['HTTP_HOST']) && $_SERVER['HTTP_HOST'] && isset($_SERVER['REQUEST_URI'])) { - $urlSuffix = " (http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI])"; - } - - return '[' . date('d-M-Y H:i:s') . "] $errtype at $relfile line $errline: $errstr$urlSuffix" . PHP_EOL; - } - -} diff --git a/dev/LogFileWriter.php b/dev/LogFileWriter.php deleted file mode 100644 index b9df69df0..000000000 --- a/dev/LogFileWriter.php +++ /dev/null @@ -1,65 +0,0 @@ -path = $path; - $this->messageType = $messageType; - $this->extraHeaders = $extraHeaders; - } - - public static function factory($path, $messageType = 3, $extraHeaders = '') { - return new SS_LogFileWriter($path, $messageType, $extraHeaders); - } - - /** - * Write the log message to the file path set - * in this writer. - */ - public function _write($event) { - if(!$this->_formatter) { - $formatter = new SS_LogErrorFileFormatter(); - $this->setFormatter($formatter); - } - $message = $this->_formatter->format($event); - if(!file_exists(dirname($this->path))) mkdir(dirname($this->path), 0755, true); - error_log($message, $this->messageType, $this->path, $this->extraHeaders); - } - -} diff --git a/dev/SysLogWriter.php b/dev/SysLogWriter.php deleted file mode 100644 index aea45f7cb..000000000 --- a/dev/SysLogWriter.php +++ /dev/null @@ -1,51 +0,0 @@ -_formatter) $this->setFormatter(new SS_LogErrorFileFormatter()); - syslog($event['priority'], $this->_formatter->format($event)); - } - -} diff --git a/dev/ZendLog.php b/dev/ZendLog.php deleted file mode 100644 index fbcfd4451..000000000 --- a/dev/ZendLog.php +++ /dev/null @@ -1,43 +0,0 @@ -_writers; - } - - /** - * Remove a writer instance that exists in 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]); - } - } - } - - /** - * Clear all writers in this logger. - */ - public function clearWriters() { - $this->_writers = array(); - } - -} diff --git a/docs/en/02_Developer_Guides/07_Debugging/01_Error_Handling.md b/docs/en/02_Developer_Guides/07_Debugging/01_Error_Handling.md index 863116696..0d02daa38 100644 --- a/docs/en/02_Developer_Guides/07_Debugging/01_Error_Handling.md +++ b/docs/en/02_Developer_Guides/07_Debugging/01_Error_Handling.md @@ -1,26 +1,79 @@ -title: Error Handling -summary: Trap, fire and report user exceptions, warnings and errors. +title: Logging and Error Handling +summary: Trap, fire and report diagnostic logs, user exceptions, warnings and errors. -# Error Handling +# Logging and Error Handling -SilverStripe has its own error trapping and handling support. On development sites, SilverStripe will deal harshly with -any warnings or errors: a full call-stack is shown and execution stops for anything, giving you early warning of a -potential issue to handle. +SilverStripe uses Monolog for both error handling and logging. It comes with two default configurations: one for +development environments, and another for test or live environments. On development environments, SilverStripe will +deal harshly with any warnings or errors: a full call-stack is shown and execution stops for anything, giving you early +warning of a potential issue to handle. -## Triggering the error handler. +## Raising errors and logging diagnostic information. -You should use [user_error](http://www.php.net/user_error) to throw errors where appropriate. +For informational and debug logs, you can use the Logger directly. The Logger is a PSR-3 compatible LoggerInterface and +can be accessed via the `Injector`: :::php - if(true == false) { - user_error("I have an error problem", E_USER_ERROR); + Injector::inst()->get('Logger')->info('User has logged in: ID #' . Member::currentUserID()); + Injector::inst()->get('Logger')->debug('Query executed: ' . $sql); + +Although you can raise more important levels of alerts in this way, we recommend using PHP's native error systems for +these instead. + +For notice-level and warning-level issues, you should use [user_error](http://www.php.net/user_error) to throw errors +where appropriate. These will not halt execution but will send a message to the + + :::php + function delete() { + if($this->alreadyDelete) { + user_error("Delete called on already deleted object", E_USER_NOTICE); + return; + } + ... + } + + function getRelatedObject() { + if(!$this->RelatedObjectID) { + user_error("Can't find a related object", E_USER_WARNING); + return null; + } + ... } - if(0 / 0) { - user_error("This time I am warning you", E_USER_WARNING); +For errors that should halt execution, you should use Exceptions. Normally, Exceptions will halt the flow of executuion, +but they can be caught with a try/catch clause. + + :::php + throw new \LogicException("Query failed: " . $sql); + +### Accessing the logger via dependency injection. + +It can quite verbose to call `Injector::inst()->get('Logger')` all the time. More importantly, it also means that you're +coupling your code to global state, which is a bad design practise. A better approach is to use depedency injection to +pass the logger in for you. The [Injector](../extending/Injector) can help with this. The most straightforward is to +specify a `dependencies` config setting, like this: + + :::php + class MyController { + + private static $dependencies = array( + 'logger' => '%$Logger', + ); + + // This will be set automatically, as long as MyController is instantiated via Injector + public $logger; + + function init() { + $this->logger->debug("MyController::init() called"); + parent::init(); + } + } -## Error Levels +In other contexts, such as testing or batch processing, logger can be set to a different value by the code calling +MyController. + +### Error Levels * **E_USER_WARNING:** Err on the side of over-reporting warnings. Throwing warnings provides a means of ensuring that developers know: @@ -31,24 +84,115 @@ developers know: * **E_USER_ERROR:** Throwing one of these errors is going to take down the production site. So you should only throw E_USER_ERROR if it's going to be **dangerous** or **impossible** to continue with the request. +## Configuring error logging -## Filesystem Logs +You can configure your logging using Monolog handlers. The handlers should be provided int the `Logger.handlers` +configuration setting. Below we have a couple of common examples, but Monolog comes with [many different handlers](https://github.com/Seldaek/monolog/blob/master/doc/02-handlers-formatters-processors.md#handlers) +for you to try. -You can indicate a log file relative to the site root. +### Sending emails -**mysite/_config.php** +To send emails, you can use Monolog's [NativeMailerHandler](https://github.com/Seldaek/monolog/blob/master/src/Monolog/Handler/NativeMailerHandler.php#L74), like this: - :::php - if(!Director::isDev()) { - // log errors and warnings - SS_Log::add_writer(new SS_LogFileWriter('../silverstripe-errors-warnings.log'), SS_Log::WARN, '<='); - - // or just errors - SS_Log::add_writer(new SS_LogFileWriter('../silverstripe-errors.log'), SS_Log::ERR); - - // or notices (e.g. for Deprecation Notifications) - SS_Log::add_writer(new SS_LogFileWriter('../silverstripe-errors-notices.log'), SS_Log::NOTICE); - } + Injector: + Logger: + calls: + MailHandler: [ pushHandler, [ %$MailHandler ] ] + MailHandler: + class: Monolog\Handler\NativeMailerHandler + constructor: + - me@example.com + - There was an error on your test site + - me@example.com + - error + properties: + ContentType: text/html + Formatter: %$SilverStripe\Framework\Logging\DetailedErrorFormatter + +The first section 4 lines passes a new handler to `Logger::pushHandler()` from the named service `MailHandler`. The +next 10 lines define what the service is. + +The calls key, `MailHandler`, can be anything you like: its main purpose is to let other configuration disable it +(see below). + +### Logging to a file + +To log to a file, you can use Monolog's [StreamHandler](https://github.com/Seldaek/monolog/blob/master/src/Monolog/Handler/StreamHandler.php#L74), like this: + + Injector: + Logger: + calls: + LogFileHandler: [ pushHandler, [ %$LogFileHandler ] ] + LogFileHandler: + class: Monolog\Handler\StreamHandler + constructor: + - "../silverstripe.log" + - "info" + +The log file will be relative to the framework/ path, so "../silverstripe.log" will create a file in your project root. + +### Disabling the default handler + +You can disable a handler by removing its pushHandlers call from the calls option of the Logger service definition. +The handler key of the default handler is `DisplayErrorHandler`, so you can disable it like this: + + Injector: + Logger: + calls: + DisplayErrorHandler: %%remove%% + +### Setting a different configuration for dev + +In order to set different logging configuration on different environment types, we rely on the environment-specific +configuration features that the config system proviers. For example, here we have different configuration for dev and +non-dev. + + --- + Name: dev-errors + Only: + environment: dev + --- + Injector: + Logger: + calls: + - [ pushHandler, [ %$DisplayErrorHandler ]] + + DisplayErrorHandler: + class: SilverStripe\Framework\Logging\HTTPOutputHandler + constructor: + - "notice" + properties: + Formatter: %$SilverStripe\Framework\Logging\DetailedErrorFormatter + --- + Name: live-errors + Except: + environment: dev + --- + Injector: + Logger: + calls: + - [ pushHandler, [ %$LogFileHandler ]] + - [ pushHandler, [ %$DisplayErrorHandler ]] + + LogFileHander: + class: Monolog\Handler\StreamHandler + constructor: + - "../silverstripe.log" + - "notice" + properties: + Formatter: %$Monolog\Formatter\HtmlFormatter + ContentType: text/html + DisplayErrorHandler: + class: SilverStripe\Framework\Logging\HTTPOutputHandler + constructor: + - "error" + properties: + Formatter: %$FriendlyErrorFormatter + FriendlyErrorFormatter: + class: SilverStripe\Framework\Logging\DebugViewFriendlyErrorFormatter + properties: + Title: "There has been an error" + Body: "The website server has not been able to respond to your request"
In addition to SilverStripe-integrated logging, it is advisable to fall back to PHPs native logging functionality. A @@ -71,6 +215,48 @@ You can send both fatal errors and warnings in your code to a specified email-ad SS_Log::add_writer(new SS_LogEmailWriter('admin@domain.com'), SS_Log::ERR); } -## API Documentation -* [api:SS_Log] +## Replacing default implementations + +For most application, Monolog and its default error handler should be fine, as you can get a lot of flexibility simply +by changing that handlers that are used. However, some situations will call for replacing the default components with +others. + +### Replacing the logger + +Monolog comes by default with SilverStripe, but you may use another PSR-3 compliant logger, if you wish. To do this, +set the `Injector.Logger` configuration parameter, providing a new injector definition. For example: + + Injector: + ErrorHandler: + class: Logging\Logger + constructor: + - 'alternative-logger' + +If you do this, you will need to supply your own handlers, and the `Logger.handlers` configuration parameter will +be ignored. + +### Replacing the error handler + +The class `SilverStripe\Framework\Logging\MonologLoader` is responsible for loading performing Monolog-specific +configuration. It does a number of things: + + * Create a `Monolog\ErrorHandler` object. + * Register the registered service `Logger` against it, to start the error handler. + * If `Logger` has a `pushHandler()` method, pass every object defined by `ErrorHandler.handlers` into it, one at a time. + +This error handler is flexible enough to work with any PSR-3 logging implementation, but sometimes you will want to use +another. To replace this, you should registered a new service, `ErrorHandlerLoader`. For example: + + Injector: + ErrorHandlerLoader: + class: MyApp\CustomErrorHandlerLoader + +You should register something `Callable`, for example a class with an `__invoke()` method. + +## Differences from SilverStripe 3 + +In SilverStripe 3, logging was based on the Zend Log module. Customisations were added using `SS_Log::add_writer()`. +This function no longer works, and any Zend Log writers will need to be replaced with Monolog handlers. Fortunately, +a range of handlers are available, both in the core package and in add-ons. See the +[Monolog documentation](https://github.com/Seldaek/monolog/blob/master/doc/01-usage.md) for more information. diff --git a/i18n/i18n.php b/i18n/i18n.php index 171bc9e16..59358a09e 100644 --- a/i18n/i18n.php +++ b/i18n/i18n.php @@ -2114,12 +2114,12 @@ class i18n extends Object implements TemplateGlobalProvider, Flushable { $placeholder = '{'.$variable.'}'; $returnValue = str_replace($placeholder, $injection, $returnValue, $count); if(!$count) { - SS_Log::log(sprintf( + Injector::inst()->get('Logger')->log('notice', sprintf( "Couldn't find placeholder '%s' in translation string '%s' (id: '%s')", $placeholder, $returnValue, $entity - ), SS_Log::NOTICE); + )); } } } diff --git a/logging/DebugViewFriendlyErrorFormatter.php b/logging/DebugViewFriendlyErrorFormatter.php new file mode 100644 index 000000000..9172fbc5d --- /dev/null +++ b/logging/DebugViewFriendlyErrorFormatter.php @@ -0,0 +1,90 @@ +statusCode; + } + + public function setStatusCode($statusCode) { + $this->statusCode = $statusCode; + } + + public function getTitle($title) { + return $this->friendlyErrorMessage; + } + + public function setTitle($title) { + $this->friendlyErrorMessage = $title; + } + + public function getBody($title) { + return $this->friendlyErrorDetail; + } + + public function setBody($body) { + $this->friendlyErrorDetail = $body; + } + + public function format(array $record) + { + + return $this->output(); + } + + public function formatBatch(array $records) { + return $this->output(); + } + + public function output() { + // TODO: Refactor into a content-type option + if(\Director::is_ajax()) { + return $this->friendlyErrorMessage; + + } else { + // TODO: Refactor this into CMS + if(class_exists('ErrorPage')){ + $errorFilePath = \ErrorPage::get_filepath_for_errorcode( + $this->statusCode, + class_exists('Translatable') ? \Translatable::get_current_locale() : null + ); + + if(file_exists($errorFilePath)) { + $content = file_get_contents($errorFilePath); + if(!headers_sent()) { + header('Content-Type: text/html'); + } + // $BaseURL is left dynamic in error-###.html, so that multi-domain sites don't get broken + return str_replace('$BaseURL', \Director::absoluteBaseURL(), $content); + } + } + + + $renderer = \Debug::create_debug_view(); + $output = $renderer->renderHeader(); + $output .= $renderer->renderInfo("Website Error", $this->friendlyErrorMessage, $this->friendlyErrorDetail); + + if(\Email::config()->admin_email) { + $mailto = \Email::obfuscate(\Email::config()->admin_email); + $output .= $renderer->renderParagraph('Contact an administrator: ' . $mailto . ''); + } + + $output .= $renderer->renderFooter(); + return $output; + } + } +} diff --git a/logging/DetailedErrorFormatter.php b/logging/DetailedErrorFormatter.php new file mode 100644 index 000000000..a13182e65 --- /dev/null +++ b/logging/DetailedErrorFormatter.php @@ -0,0 +1,118 @@ + $exception->getCode(), + 'message' => 'Uncaught ' . get_class($exception) . ': ' . $exception->getMessage(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => $exception->getTrace(), + ); + } else { + $context = $record['context']; + foreach(array('code','message','file','line') as $key) { + if(!isset($context[$key])) { + $context[$key] = isset($record[$key]) ? $record[$key] : null; + } + } + + $trace = debug_backtrace(); + + // Filter out monolog plumbing from the trace + // If the context file & line isn't found in the trace, then the trace is most likely + // call to the fatal error handler and is not useful, so exclude it entirely + $i = $this->findInTrace($trace, $context['file'], $context['line']); + if($i !== null) { + $context['trace'] = array_slice($trace, $i); + } else { + $context['trace'] = null; + } + } + + return $this->output( + $context['code'], + $context['message'], + $context['file'], + $context['line'], + $context['trace'] + ); + } + + public function formatBatch(array $records) { + return implode("\n", array_map(array($this, 'format'), $records)); + } + + /** + * Find a call on the given file & line in the trace + * @param array $trace The result of debug_backtrace() + * @param string $file The filename to look for + * @param string $line The line number to look for + * @return int|null The matching row number, if found, or null if not found + */ + protected function findInTrace(array $trace, $file, $line) { + foreach($trace as $i => $call) { + if(isset($call['file']) && isset($call['line']) && $call['file'] == $file && $call['line'] == $line) { + return $i; + } + } + return null; + } + + /** + * Render a developer facing error page, showing the stack trace and details + * of the code where the error occured. + * + * @param int $errno + * @param str $errstr + * @param str $errfile + * @param int $errline + * @param array $errcontext + */ + protected function output($errno, $errstr, $errfile, $errline, $errcontext) { + $reporter = \Debug::create_debug_view(); + + // Coupling alert: This relies on knowledge of how the director gets its URL, it could be improved. + $httpRequest = null; + if(isset($_SERVER['REQUEST_URI'])) { + $httpRequest = $_SERVER['REQUEST_URI']; + } elseif(isset($_REQUEST['url'])) { + $httpRequest = $_REQUEST['url']; + } + if(isset($_SERVER['REQUEST_METHOD'])) { + $httpRequest = $_SERVER['REQUEST_METHOD'] . ' ' . $httpRequest; + } + + $output = $reporter->renderHeader($httpRequest); + $output .= $reporter->renderError($httpRequest, $errno, $errstr, $errfile, $errline); + + if(file_exists($errfile)) { + $lines = file($errfile); + + // Make the array 1-based + array_unshift($lines, ""); + unset($lines[0]); + + $offset = $errline-10; + $lines = array_slice($lines, $offset, 16, true); + $output .= $reporter->renderSourceFragment($lines, $errline); + } + $output .= $reporter->renderTrace($errcontext); + $output .= $reporter->renderFooter(); + + return $output; + } +} diff --git a/logging/HTTPOutputHandler.php b/logging/HTTPOutputHandler.php new file mode 100644 index 000000000..6427ba451 --- /dev/null +++ b/logging/HTTPOutputHandler.php @@ -0,0 +1,73 @@ +contentType; + } + + /** + * Set the mime type to use when displaying this error. + * Default text/html + */ + public function setContentType($contentType) { + $this->contentType = $contentType; + } + + /** + * Get the HTTP status code to use when displaying this error. + */ + public function getStatusCode() { + return $this->statusCode; + } + + /** + * Set the HTTP status code to use when displaying this error. + * Default 500 + */ + public function setStatusCode($statusCode) { + $this->statusCode = $statusCode; + } + + protected function write(array $record) { + ini_set('display_errors', 0); + + // TODO: This coupling isn't ideal + // See https://github.com/silverstripe/silverstripe-framework/issues/4484 + if(\Controller::has_curr()) { + $response = \Controller::curr()->getResponse(); + } else { + $response = new SS_HTTPResponse(); + } + + // If headers have been sent then these won't be used, and may throw errors that we wont' want to see. + if(!headers_sent()) { + $response->setStatusCode($this->statusCode); + $response->addHeader("Content-Type", $this->contentType); + } else { + // To supress errors aboot errors + $response->setStatusCode(200); + } + + $response->setBody($record['formatted']); + $response->output(); + + return false === $this->bubble; + } +} diff --git a/logging/Log.php b/logging/Log.php new file mode 100644 index 000000000..42ace5530 --- /dev/null +++ b/logging/Log.php @@ -0,0 +1,89 @@ + + * SS_Log::add_writer(new SS_LogEmailWriter('my@email.com'), SS_Log::ERR); + * + * + * Example usage of logging errors by file: + * + * SS_Log::add_writer(new SS_LogFileWriter('/var/log/silverstripe/errors.log'), SS_Log::ERR); + * + * + * Example usage of logging at warnings and errors by setting the priority to '<=': + * + * SS_Log::add_writer(new SS_LogEmailWriter('my@email.com'), SS_Log::WARN, '<='); + * + * + * Each writer object can be assigned a formatter. The formatter is + * responsible for formatting the message before giving it to the writer. + * {@link SS_LogErrorEmailFormatter} is such an example that formats errors + * into HTML for human readability in an email client. + * + * Formatters are added to writers like this: + * + * $logEmailWriter = new SS_LogEmailWriter('my@email.com'); + * $myEmailFormatter = new MyLogEmailFormatter(); + * $logEmailWriter->setFormatter($myEmailFormatter); + * + * + * @package framework + * @subpackage dev + */ +class SS_Log +{ + + const ERR = 'error'; + const WARN = 'warning'; + const NOTICE = 'notice'; + const INFO = 'info'; + const DEBUG = 'debug'; + + /** + * Get the logger currently in use, or create a new one if it doesn't exist. + * + * @return Psr\Log\LoggerInterface + */ + public static function get_logger() { + Deprecation::notice('4.0', 'Use Injector::inst()->get(\'Logger\') instead'); + return Injector::inst()->get('Logger'); + } + + public static function add_writer($writer) { + throw new \InvalidArgumentException("SS_Log::add_writer() on longer works. Please use a Monolog Handler " + ."instead, and add list of handlers in the Logger.handlers configuration parameter."); + } + + /** + * 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 mixed $message Exception object or array of error context variables + * @param const $priority Priority. Possible values: SS_Log::ERR, SS_Log::WARN or SS_Log::NOTICE + * @param mixed $extras Extra information to log in event + * + * @deprecated 4.0.0:5.0.0 Use Injector::inst()->get('Logger')->log($priority, $message) instead + */ + public static function log($message, $priority, $extras = null) { + Deprecation::notice('4.0', 'Use Injector::inst()->get(\'Logger\')->log($priority, $message) instead'); + + Injector::inst()->get('Logger')->log($priority, $message); + } +} diff --git a/logging/MonologErrorHandler.php b/logging/MonologErrorHandler.php new file mode 100644 index 000000000..c51e43298 --- /dev/null +++ b/logging/MonologErrorHandler.php @@ -0,0 +1,30 @@ +logger = $logger; + } + + function start() { + if(!$this->logger) { + throw new \InvalidArgumentException("No Logger property passed to MonologErrorHandler." + . "Is your Injector config correct?"); + } + + ErrorHandler::register($this->logger); + } +} diff --git a/tests/dev/LogTest.php b/tests/dev/LogTest.php deleted file mode 100644 index 4ab6d1ef1..000000000 --- a/tests/dev/LogTest.php +++ /dev/null @@ -1,138 +0,0 @@ -assertEquals(2, count($writers)); - } - - public function testRemoveWriter() { - $testEmailWriter = new SS_LogEmailWriter('test@test.com'); - $testFileWriter = new SS_LogFileWriter('../test.log'); - SS_Log::add_writer($testEmailWriter, SS_Log::ERR); - SS_Log::add_writer($testFileWriter, SS_Log::WARN); - - SS_Log::remove_writer($testEmailWriter); - $writers = SS_Log::get_writers(); - - $this->assertEquals(1, count($writers)); - - SS_Log::remove_writer($testFileWriter); - $writers = SS_Log::get_writers(); - $this->assertEquals(0, count($writers)); - } - - public function testEmailWriter() { - $testEmailWriter = new SS_LogEmailWriter('test@test.com'); - SS_Log::add_writer($testEmailWriter, SS_Log::ERR); - - SS_Log::log('Email test', SS_Log::ERR, array('my-string' => 'test', 'my-array' => array('one' => 1))); - $this->assertEmailSent('test@test.com'); - $email = $this->findEmail('test@test.com'); - $this->assertContains('[Error] Email test', $email['htmlContent']); - $parser = new CSSContentParser($email['htmlContent']); - $extras = $parser->getBySelector('table.extras'); - $extraRows = $extras[0]->tr; - $this->assertContains('my-string', $extraRows[count($extraRows)-2]->td[0]->asXML(), 'Contains extra data key'); - $this->assertContains('test', $extraRows[count($extraRows)-2]->td[1]->asXML(), 'Contains extra data value'); - $this->assertContains('my-array', $extraRows[count($extraRows)-1]->td[0]->asXML(), 'Contains extra data key'); - $this->assertContains( - "array('one'=>1,)", - str_replace(array("\r", "\n", " "), '', $extraRows[count($extraRows)-1]->td[1]->asXML()), - 'Serializes arrays correctly' - ); - } - - public function testEmailWriterDebugPriority() { - $testEmailWriter = new SS_LogEmailWriter('test@test.com'); - SS_Log::add_writer($testEmailWriter, SS_Log::DEBUG); - SS_Log::log('Test something', SS_Log::DEBUG, array('my-string' => 'test', 'my-array' => array('one' => 1))); - $this->assertEmailSent('test@test.com'); - $email = $this->findEmail('test@test.com'); - $this->assertContains('[DEBUG] Test something', $email['htmlContent']); - } - - public function testEmailWriterInfoPriority() { - $testEmailWriter = new SS_LogEmailWriter('test@test.com'); - SS_Log::add_writer($testEmailWriter, SS_Log::INFO); - SS_Log::log('Test something', SS_Log::INFO, array('my-string' => 'test', 'my-array' => array('one' => 1))); - $this->assertEmailSent('test@test.com'); - $email = $this->findEmail('test@test.com'); - $this->assertContains('[INFO] Test something', $email['htmlContent']); - } - - protected function exceptionGeneratorThrower() { - throw new Exception("thrown from SS_LogTest::testExceptionGeneratorTop"); - } - - protected function exceptionGenerator() { - $this->exceptionGeneratorThrower(); - } - - public function testEmailException() { - $testEmailWriter = new SS_LogEmailWriter('test@test.com'); - SS_Log::add_writer($testEmailWriter, SS_Log::ERR); - - // Trigger exception handling mechanism - try { - $this->exceptionGenerator(); - } catch(Exception $exception) { - // Mimics exceptionHandler, but without the exit(1) - SS_Log::log( - array( - 'errno' => E_USER_ERROR, - 'errstr' => ("Uncaught " . get_class($exception) . ": " . $exception->getMessage()), - 'errfile' => $exception->getFile(), - 'errline' => $exception->getLine(), - 'errcontext' => $exception->getTrace() - ), - SS_Log::ERR - ); - } - - // Ensure email is sent - $this->assertEmailSent('test@test.com'); - - // Begin parsing of email body - $email = $this->findEmail('test@test.com'); - $parser = new CSSContentParser($email['htmlContent']); - - // Check that the first three lines of the stacktrace are correct - $stacktrace = $parser->getByXpath('//body/div[1]/ul[1]'); - $this->assertContains('SS_LogTest->exceptionGeneratorThrower()', $stacktrace[0]->li[0]->asXML()); - $this->assertContains('SS_LogTest->exceptionGenerator()', $stacktrace[0]->li[1]->asXML()); - $this->assertContains('SS_LogTest->testEmailException()', $stacktrace[0]->li[2]->asXML()); - } - - public function testSubclassedLogger() { - $this->assertTrue(SS_Log::get_logger() !== SS_LogTest_NewLogger::get_logger()); - } - -} - -class SS_LogTest_NewLogger extends SS_Log { - protected static $logger; -}