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

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.
This commit is contained in:
Sam Minnee 2015-07-24 10:53:41 +12:00
parent ebc3900c4a
commit 1b8d295767
22 changed files with 713 additions and 1096 deletions

45
_config/logging.yml Normal file
View File

@ -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"

View File

@ -17,7 +17,8 @@
], ],
"require": { "require": {
"php": ">=5.4.0", "php": ">=5.4.0",
"composer/installers": "~1.0" "composer/installers": "~1.0",
"monolog/monolog": "~1.11"
}, },
"require-dev": { "require-dev": {
"phpunit/PHPUnit": "~3.7" "phpunit/PHPUnit": "~3.7"

View File

@ -50,6 +50,9 @@
* @subpackage core * @subpackage core
*/ */
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
/* /*
* _ss_environment.php handler * _ss_environment.php handler
*/ */
@ -139,7 +142,12 @@ if(defined('SS_USE_BASIC_AUTH') && SS_USE_BASIC_AUTH) {
} }
if(defined('SS_ERROR_LOG')) { 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 // Allow database adapters to handle their own configuration

View File

@ -271,7 +271,10 @@ EOT
// an error, and the response doesn't have any body yet that might contain // an error, and the response doesn't have any body yet that might contain
// a more specific error description. // a more specific error description.
if(Director::isLive() && $this->isError() && !$this->body) { 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 { } else {
echo $this->body; echo $this->body;
} }

View File

@ -70,9 +70,8 @@ require_once 'view/TemplateGlobalProvider.php';
require_once 'control/Director.php'; require_once 'control/Director.php';
require_once 'dev/Debug.php'; require_once 'dev/Debug.php';
require_once 'dev/DebugView.php'; require_once 'dev/DebugView.php';
require_once 'dev/CliDebugView.php';
require_once 'dev/Backtrace.php'; require_once 'dev/Backtrace.php';
require_once 'dev/ZendLog.php';
require_once 'dev/Log.php';
require_once 'filesystem/FileFinder.php'; require_once 'filesystem/FileFinder.php';
require_once 'core/manifest/ManifestCache.php'; require_once 'core/manifest/ManifestCache.php';
require_once 'core/manifest/ClassLoader.php'; require_once 'core/manifest/ClassLoader.php';
@ -136,8 +135,9 @@ if(Director::isLive()) {
/** /**
* Load error handlers * Load error handlers
*/ */
Debug::loadErrorHandlers();
$errorHandler = Injector::inst()->get('ErrorHandler');
$errorHandler->start();
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// HELPER FUNCTIONS // HELPER FUNCTIONS

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Supports debugging and core error handling. * Supports debugging and core error handling.
* *
@ -24,28 +25,6 @@
*/ */
class Debug { class Debug {
/**
* @config
* @var String indicating the file where errors are logged.
* Filename is relative to the site root.
* The named file will have a terse log sent to it, and the full log (an
* encoded file containing backtraces and things) will go to a file of a similar
* name, but with the suffix ".full" added.
*/
private static $log_errors_to = null;
/**
* @config
* @var string The header of the message shown to users on the live site when a fatal error occurs.
*/
private static $friendly_error_header = 'There has been an error';
/**
* @config
* @var string The body of the message shown to users on the live site when a fatal error occurs.
*/
private static $friendly_error_detail = 'The website server has not been able to respond to your request.';
/** /**
* Show the contents of val in a debug-friendly way. * Show the contents of val in a debug-friendly way.
* Debug::show() is intended to be equivalent to dprintr() * Debug::show() is intended to be equivalent to dprintr()
@ -169,204 +148,6 @@ class Debug {
} }
} }
// Keep track of how many headers have been sent
private static $headerCount = 0;
/**
* Send a debug message in an HTTP header. Only works if you are
* on Dev, and headers have not yet been sent.
*
* @param string $msg
* @param string $prefix (optional)
* @return void
*/
public static function header($msg, $prefix = null) {
if (Director::isDev() && !headers_sent()) {
self::$headerCount++;
header('SS-'.self::$headerCount.($prefix?'-'.$prefix:'').': '.$msg);
}
}
/**
* Log to a standard text file output.
*
* @param $message string to output
*/
public static function log($message) {
if (defined('BASE_PATH')) {
$path = BASE_PATH;
}
else {
$path = dirname(__FILE__) . '/../..';
}
$file = $path . '/debug.log';
$now = date('r');
$content = "\n\n== $now ==\n$message\n";
file_put_contents($file, $content, FILE_APPEND);
}
/**
* Load error handlers into environment.
* Caution: The error levels default to E_ALL is the site is in dev-mode (set in main.php).
*/
public static function loadErrorHandlers() {
set_error_handler('errorHandler', error_reporting());
set_exception_handler('exceptionHandler');
}
public static function noticeHandler($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::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. * Create an instance of an appropriate DebugView object.
* *
@ -379,84 +160,6 @@ class Debug {
return Injector::inst()->get($service); 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 '<pre>';
$offset++;
foreach($lines as $line) {
$line = htmlentities($line, ENT_COMPAT, 'UTF-8');
if ($offset == $errline) {
echo "<span>$offset</span> <span class=\"error\">$line</span>";
} else {
echo "<span>$offset</span> $line";
}
$offset++;
}
echo '</pre>';
}
/** /**
* Check if the user has permissions to run URL debug tools, * Check if the user has permissions to run URL debug tools,
* else redirect them to log in. * else redirect them to log in.
@ -501,65 +204,3 @@ class Debug {
die(); 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);
}
}

View File

@ -23,6 +23,38 @@ class DebugView extends Object
private static $columns = 100; private static $columns = 100;
protected static $error_types = array( 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( E_USER_ERROR => array(
'title' => 'User Error', 'title' => 'User Error',
'class' => 'error' 'class' => 'error'

View File

@ -1,185 +0,0 @@
<?php
require_once 'Zend/Log.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.
*
* These priorities are currently supported:
* - SS_Log::ERR
* - SS_Log::WARN
* - SS_Log::NOTICE
*
* You can add an error writer by calling {@link SS_Log::add_writer()}
*
* Example usage of logging errors by email notification:
* <code>
* SS_Log::add_writer(new SS_LogEmailWriter('my@email.com'), SS_Log::ERR);
* </code>
*
* Example usage of logging errors by file:
* <code>
* SS_Log::add_writer(new SS_LogFileWriter('/var/log/silverstripe/errors.log'), SS_Log::ERR);
* </code>
*
* Example usage of logging at warnings and errors by setting the priority to '<=':
* <code>
* SS_Log::add_writer(new SS_LogEmailWriter('my@email.com'), SS_Log::WARN, '<=');
* </code>
*
* 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:
* <code>
* $logEmailWriter = new SS_LogEmailWriter('my@email.com');
* $myEmailFormatter = new MyLogEmailFormatter();
* $logEmailWriter->setFormatter($myEmailFormatter);
* </code>
*
* @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
}
}
}

View File

@ -1,94 +0,0 @@
<?php
require_once 'Zend/Log/Writer/Abstract.php';
/**
* Sends an error message to an email.
*
* @see SS_Log for more information on using writers.
*
* @package framework
* @subpackage dev
*/
class SS_LogEmailWriter extends Zend_Log_Writer_Abstract {
/**
* @config
* @var $send_from Email address to send log information from
*/
private static $send_from = 'errors@silverstripe.com';
protected $emailAddress;
protected $customSmtpServer;
public function __construct($emailAddress, $customSmtpServer = false) {
$this->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);
}
}

View File

@ -1,80 +0,0 @@
<?php
require_once 'Zend/Log/Formatter/Interface.php';
/**
* Formats SS error emails with a basic layout.
*
* @package framework
* @subpackage dev
*/
class SS_LogErrorEmailFormatter implements Zend_Log_Formatter_Interface {
public function format($event) {
switch($event['priorityName']) {
case 'ERR':
$errorType = 'Error';
$colour = 'red';
break;
case 'WARN':
$errorType = 'Warning';
$colour = 'orange';
break;
case 'NOTICE':
$errorType = 'Notice';
$colour = 'grey';
break;
default:
$errorType = $event['priorityName'];
$colour = 'grey';
}
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 = '';
$data .= '<style type="text/css">html, body, table {font-family: sans-serif; font-size: 12px;}</style>';
$data .= "<div style=\"border: 5px $colour solid;\">\n";
$data .= "<p style=\"color: white; background-color: $colour; margin: 0\">[$errorType] ";
$data .= nl2br(htmlspecialchars($errstr))."<br />$errfile:$errline\n<br />\n<br />\n</p>\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 .= "<h3>Details</h3>\n";
$data .= "<table class=\"extras\">\n";
foreach($extras as $k => $v) {
if(is_array($v)) $v = var_export($v, true);
$data .= sprintf(
"<tr><td><strong>%s</strong></td><td><pre>%s</pre></td></tr>\n", $k, $v);
}
$data .= "</table>\n";
}
$data .= "</div>\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
);
}
}

View File

@ -1,43 +0,0 @@
<?php
require_once 'Zend/Log/Formatter/Interface.php';
/**
* Formats SS error entries in an error file log.
* Format: [d-M-Y h:i:s] <type> at <file> line <line>: <errormessage> <url>
* @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;
}
}

View File

@ -1,65 +0,0 @@
<?php
require_once 'Zend/Log/Writer/Abstract.php';
/**
* Writes an error message to a file.
*
* Note: You need to make sure your web server is able
* to write to the file path that you specify to write
* logs to.
*
* @uses error_log() built-in PHP function.
* @see SS_Log for more information on using writers.
*
* @package framework
* @subpackage dev
*/
class SS_LogFileWriter extends Zend_Log_Writer_Abstract {
/**
* The path to the file that errors will be stored in.
* For example, "/var/logs/silverstripe/errors.log".
*
* @var string
*/
protected $path;
/**
* Message type to pass to error_log()
* @see http://us3.php.net/manual/en/function.error-log.php
* @var int
*/
protected $messageType;
/**
* Extra headers to pass to error_log()
* @see http://us3.php.net/manual/en/function.error-log.php
* @var string
*/
protected $extraHeaders;
public function __construct($path, $messageType = 3, $extraHeaders = '') {
$this->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);
}
}

View File

@ -1,51 +0,0 @@
<?php
require_once 'Zend/Log/Writer/Abstract.php';
/**
* Sends an error message to the system log whenever an
* error occurs.
*
* @see SS_Log for more information on using writers
* @uses Zend_Log_Writer_Abstract
* @package framework
* @subpackage dev
*/
class SS_SysLogWriter extends Zend_Log_Writer_Abstract {
/**
* @param string $ident Identity of log, defaults to "Silverstripe_log" if null
* @param $options Option constants, passed to openlog()
* @param $facility Type of program logging the message, passed to openlog()
*/
public function __construct($ident = null, $options = null, $facility = LOG_LOCAL0) {
if(!$ident) $ident = 'SilverStripe_log';
if(!$options) $options = LOG_PID | LOG_PERROR;
openlog($ident, $options, $facility);
}
/**
* Close the log when this object is destroyed.
*/
public function __destruct() {
closelog();
}
/**
* @param $option See {@link __construct}
* @return SS_SysLogWriter
*/
static public function factory($config) {
return new SS_SysLogWriter(null, $config);
}
/**
* Write to the system log with the event details.
* @param array $event Error details
*/
public function _write($event) {
// If no formatter set up, use default then log the event
if(!$this->_formatter) $this->setFormatter(new SS_LogErrorFileFormatter());
syslog($event['priority'], $this->_formatter->format($event));
}
}

View File

@ -1,43 +0,0 @@
<?php
require_once 'Zend/Log.php';
/**
* Extensions to Zend_Log to make it work nicer
* with {@link SS_Log}.
*
* Please refer to {@link SS_Log} for information on
* setting up logging for your projects.
*
* @package framework
* @subpackage dev
*/
class SS_ZendLog extends Zend_Log {
/**
* Get all writers in this logger.
* @return array of Zend_Log_Writer_Abstract instances
*/
public function getWriters() {
return $this->_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();
}
}

View File

@ -1,26 +1,79 @@
title: Error Handling title: Logging and Error Handling
summary: Trap, fire and report user exceptions, warnings and errors. 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 SilverStripe uses Monolog for both error handling and logging. It comes with two default configurations: one for
any warnings or errors: a full call-stack is shown and execution stops for anything, giving you early warning of a development environments, and another for test or live environments. On development environments, SilverStripe will
potential issue to handle. 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 :::php
if(true == false) { Injector::inst()->get('Logger')->info('User has logged in: ID #' . Member::currentUserID());
user_error("I have an error problem", E_USER_ERROR); 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;
}
...
} }
if(0 / 0) { function getRelatedObject() {
user_error("This time I am warning you", E_USER_WARNING); if(!$this->RelatedObjectID) {
user_error("Can't find a related object", E_USER_WARNING);
return null;
}
...
} }
## Error Levels 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();
}
}
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 * **E_USER_WARNING:** Err on the side of over-reporting warnings. Throwing warnings provides a means of ensuring that
developers know: 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:** 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. 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 Injector:
if(!Director::isDev()) { Logger:
// log errors and warnings calls:
SS_Log::add_writer(new SS_LogFileWriter('../silverstripe-errors-warnings.log'), SS_Log::WARN, '<='); 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
// or just errors The first section 4 lines passes a new handler to `Logger::pushHandler()` from the named service `MailHandler`. The
SS_Log::add_writer(new SS_LogFileWriter('../silverstripe-errors.log'), SS_Log::ERR); next 10 lines define what the service is.
// or notices (e.g. for Deprecation Notifications) The calls key, `MailHandler`, can be anything you like: its main purpose is to let other configuration disable it
SS_Log::add_writer(new SS_LogFileWriter('../silverstripe-errors-notices.log'), SS_Log::NOTICE); (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"
<div class="info" markdown="1"> <div class="info" markdown="1">
In addition to SilverStripe-integrated logging, it is advisable to fall back to PHPs native logging functionality. A 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); 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.

View File

@ -2114,12 +2114,12 @@ class i18n extends Object implements TemplateGlobalProvider, Flushable {
$placeholder = '{'.$variable.'}'; $placeholder = '{'.$variable.'}';
$returnValue = str_replace($placeholder, $injection, $returnValue, $count); $returnValue = str_replace($placeholder, $injection, $returnValue, $count);
if(!$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')", "Couldn't find placeholder '%s' in translation string '%s' (id: '%s')",
$placeholder, $placeholder,
$returnValue, $returnValue,
$entity $entity
), SS_Log::NOTICE); ));
} }
} }
} }

View File

@ -0,0 +1,90 @@
<?php
namespace SilverStripe\Framework\Logging;
use Monolog\Logger;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Formatter\FormatterInterface;
/**
* Produce a friendly error message
*/
class DebugViewFriendlyErrorFormatter implements FormatterInterface
{
protected $statusCode = 500;
protected $friendlyErrorMessage = 'Error';
protected $friendlyErrorDetail;
public function getStatusCode() {
return $this->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;
}
}
}

View File

@ -0,0 +1,118 @@
<?php
namespace SilverStripe\Framework\Logging;
use Monolog\Logger;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Formatter\FormatterInterface;
/**
* Monolog-compatible error handler that will output a detailed error message to the screen.
*/
class DetailedErrorFormatter implements FormatterInterface
{
public function format(array $record)
{
if(isset($record['context']['exception'])) {
$exception = $record['context']['exception'];
$context = array(
'code' => $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;
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace SilverStripe\Framework\Logging;
use Monolog\Logger;
use Monolog\Handler\AbstractProcessingHandler;
/**
* Output the error to the browser, with the given HTTP status code.
* We recommend that you use a formatter that generates HTML with this.
*/
class HTTPOutputHandler extends AbstractProcessingHandler
{
private $contentType = "text/html";
private $statusCode = 500;
/**
* Get the mime type to use when displaying this error.
*/
public function getContentType() {
return $this->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;
}
}

89
logging/Log.php Normal file
View File

@ -0,0 +1,89 @@
<?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.
*
* These priorities are currently supported:
* - SS_Log::ERR
* - SS_Log::WARN
* - SS_Log::NOTICE
*
* You can add an error writer by calling {@link SS_Log::add_writer()}
*
* Example usage of logging errors by email notification:
* <code>
* SS_Log::add_writer(new SS_LogEmailWriter('my@email.com'), SS_Log::ERR);
* </code>
*
* Example usage of logging errors by file:
* <code>
* SS_Log::add_writer(new SS_LogFileWriter('/var/log/silverstripe/errors.log'), SS_Log::ERR);
* </code>
*
* Example usage of logging at warnings and errors by setting the priority to '<=':
* <code>
* SS_Log::add_writer(new SS_LogEmailWriter('my@email.com'), SS_Log::WARN, '<=');
* </code>
*
* 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:
* <code>
* $logEmailWriter = new SS_LogEmailWriter('my@email.com');
* $myEmailFormatter = new MyLogEmailFormatter();
* $logEmailWriter->setFormatter($myEmailFormatter);
* </code>
*
* @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);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace SilverStripe\Framework\Logging;
use Psr\Log\LoggerInterface;
use Monolog\ErrorHandler;
/**
* Simple adaptor to start Monolog\ErrorHandler
*/
class MonologErrorHandler
{
private $logger;
/**
* Set the PSR-3 logger to send errors & exceptions to
*/
function setLogger(LoggerInterface $logger) {
$this->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);
}
}

View File

@ -1,138 +0,0 @@
<?php
/**
* @package framework
* @subpackage tests
*/
class SS_LogTest extends SapphireTest {
protected $testEmailWriter;
public function setUp() {
parent::setUp();
SS_Log::clear_writers();
}
public function tearDown() {
parent::tearDown();
SS_Log::clear_writers();
}
public function testExistingWriter() {
$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);
$writers = SS_Log::get_writers();
$this->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'=&gt;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('<b>SS_LogTest-&gt;exceptionGeneratorThrower()</b>', $stacktrace[0]->li[0]->asXML());
$this->assertContains('<b>SS_LogTest-&gt;exceptionGenerator()</b>', $stacktrace[0]->li[1]->asXML());
$this->assertContains('<b>SS_LogTest-&gt;testEmailException()</b>', $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;
}