diff options
author | Matthieu Napoli <matthieu@mnapoli.fr> | 2015-02-02 01:45:32 +0300 |
---|---|---|
committer | Matthieu Napoli <matthieu@mnapoli.fr> | 2015-02-12 00:12:19 +0300 |
commit | e122a4e37b3eef21fc6fc32ed7d31c56c38d673f (patch) | |
tree | 89aa9600a70200f1e94cf0454d62d037f0e361e2 /plugins/Monolog | |
parent | 47c0eae3f8202430ea576e09cf0ded90f783eea1 (diff) |
Moved the monolog configuration and code into a Monolog plugin
Diffstat (limited to 'plugins/Monolog')
17 files changed, 1114 insertions, 0 deletions
diff --git a/plugins/Monolog/Formatter/LineMessageFormatter.php b/plugins/Monolog/Formatter/LineMessageFormatter.php new file mode 100644 index 0000000000..6b81f2daec --- /dev/null +++ b/plugins/Monolog/Formatter/LineMessageFormatter.php @@ -0,0 +1,75 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Monolog\Formatter; + +use Monolog\Formatter\FormatterInterface; + +/** + * Formats a log message into a line of text using our custom Piwik log format. + */ +class LineMessageFormatter implements FormatterInterface +{ + /** + * The log message format string that turns a tag name, date-time and message into + * one string to log. + * + * @var string + */ + private $logMessageFormat; + + /** + * @param string $logMessageFormat + */ + public function __construct($logMessageFormat) + { + $this->logMessageFormat = $logMessageFormat; + } + + public function format(array $record) + { + $class = isset($record['extra']['class']) ? $record['extra']['class'] : ''; + $date = $record['datetime']->format('Y-m-d H:i:s'); + + $message = $this->prefixMessageWithRequestId($record); + + $message = str_replace( + array('%tag%', '%message%', '%datetime%', '%level%'), + array($class, $message, $date, $record['level_name']), + $this->logMessageFormat + ); + + $message = str_replace("\n", "\n ", $message); + + $message .= "\n"; + + return $message; + } + + public function formatBatch(array $records) + { + foreach ($records as $key => $record) { + $records[$key] = $this->format($record); + } + + return $records; + } + + private function prefixMessageWithRequestId(array $record) + { + $requestId = isset($record['extra']['request_id']) ? $record['extra']['request_id'] : ''; + + $message = trim($record['message']); + + if ($requestId) { + $message = '[' . $requestId . '] ' . $message; + } + + return $message; + } +} diff --git a/plugins/Monolog/Handler/DatabaseHandler.php b/plugins/Monolog/Handler/DatabaseHandler.php new file mode 100644 index 0000000000..99ccd68670 --- /dev/null +++ b/plugins/Monolog/Handler/DatabaseHandler.php @@ -0,0 +1,39 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Monolog\Handler; + +use Monolog\Handler\AbstractProcessingHandler; +use Piwik\Common; +use Piwik\Db; + +/** + * Writes log to database. + */ +class DatabaseHandler extends AbstractProcessingHandler +{ + protected function write(array $record) + { + $sql = sprintf( + 'INSERT INTO %s (tag, timestamp, level, message) VALUES (?, ?, ?, ?)', + Common::prefixTable('logger_message') + ); + + $queryLog = Db::isQueryLogEnabled(); + Db::enableQueryLog(false); + + Db::query($sql, array( + $record['extra']['class'], + $record['datetime']->format('Y-m-d H:i:s'), + $record['level_name'], + trim($record['formatted']) + )); + + Db::enableQueryLog($queryLog); + } +} diff --git a/plugins/Monolog/Handler/FileHandler.php b/plugins/Monolog/Handler/FileHandler.php new file mode 100644 index 0000000000..abfd632365 --- /dev/null +++ b/plugins/Monolog/Handler/FileHandler.php @@ -0,0 +1,31 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Monolog\Handler; + +use Monolog\Handler\StreamHandler; +use Piwik\Filechecks; + +/** + * Writes log to file. + * + * Extends StreamHandler to be able to have a custom exception message. + */ +class FileHandler extends StreamHandler +{ + protected function write(array $record) + { + try { + parent::write($record); + } catch (\UnexpectedValueException $e) { + throw new \Exception( + Filechecks::getErrorMessageMissingPermissions($this->url) + ); + } + } +} diff --git a/plugins/Monolog/Handler/WebNotificationHandler.php b/plugins/Monolog/Handler/WebNotificationHandler.php new file mode 100644 index 0000000000..edd2fc76c2 --- /dev/null +++ b/plugins/Monolog/Handler/WebNotificationHandler.php @@ -0,0 +1,51 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Monolog\Handler; + +use Monolog\Handler\AbstractProcessingHandler; +use Monolog\Logger; +use Piwik\Common; +use Piwik\Notification; +use Piwik\Notification\Manager; +use Zend_Session_Exception; + +/** + * Writes log messages into HTML notification box. + */ +class WebNotificationHandler extends AbstractProcessingHandler +{ + protected function write(array $record) + { + switch ($record['level']) { + case Logger::EMERGENCY: + case Logger::ALERT: + case Logger::CRITICAL: + case Logger::ERROR: + $context = Notification::CONTEXT_ERROR; + break; + case Logger::WARNING: + $context = Notification::CONTEXT_WARNING; + break; + default: + $context = Notification::CONTEXT_INFO; + break; + } + + $message = htmlentities($record['formatted']); + + $notification = new Notification($message); + $notification->context = $context; + try { + Manager::notify(Common::getRandomString(), $notification); + } catch (Zend_Session_Exception $e) { + // Can happen if this handler is enabled in CLI + // Silently ignore the error. + } + } +} diff --git a/plugins/Monolog/Monolog.php b/plugins/Monolog/Monolog.php new file mode 100644 index 0000000000..79f42cdd41 --- /dev/null +++ b/plugins/Monolog/Monolog.php @@ -0,0 +1,15 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Monolog; + +use Piwik\Plugin; + +class Monolog extends Plugin +{ +} diff --git a/plugins/Monolog/Processor/ClassNameProcessor.php b/plugins/Monolog/Processor/ClassNameProcessor.php new file mode 100644 index 0000000000..3365f207db --- /dev/null +++ b/plugins/Monolog/Processor/ClassNameProcessor.php @@ -0,0 +1,81 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Monolog\Processor; + +use Piwik\Plugin; + +/** + * Records the name of the class that logged. + */ +class ClassNameProcessor +{ + private $skippedClasses = array( + __CLASS__, + 'Piwik\Log', + 'Piwik\Piwik', + 'Piwik\CronArchive', + 'Monolog\Logger', + ); + + public function __invoke(array $record) + { + $record['extra']['class'] = $this->getLoggingClassName(); + + return $record; + } + + /** + * Returns the name of the plugin/class that triggered the log. + * + * @return string + */ + private function getLoggingClassName() + { + $backtrace = $this->getBacktrace(); + + $name = Plugin::getPluginNameFromBacktrace($backtrace); + + // if we can't determine the plugin, use the name of the calling class + if ($name == false) { + $name = $this->getClassNameThatIsLogging($backtrace); + } + + return $name; + } + + private function getClassNameThatIsLogging($backtrace) + { + foreach ($backtrace as $line) { + if (isset($line['class'])) { + return $line['class']; + } + } + + return ''; + } + + private function getBacktrace() + { + if (version_compare(phpversion(), '5.3.6', '>=')) { + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT); + } else { + $backtrace = debug_backtrace(); + } + + $skippedClasses = $this->skippedClasses; + $backtrace = array_filter($backtrace, function ($item) use ($skippedClasses) { + if (isset($item['class'])) { + return !in_array($item['class'], $skippedClasses); + } + return true; + }); + + return $backtrace; + } +} diff --git a/plugins/Monolog/Processor/ExceptionToTextProcessor.php b/plugins/Monolog/Processor/ExceptionToTextProcessor.php new file mode 100644 index 0000000000..daaaee479e --- /dev/null +++ b/plugins/Monolog/Processor/ExceptionToTextProcessor.php @@ -0,0 +1,58 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Monolog\Processor; + +use Piwik\ErrorHandler; +use Piwik\Log; + +/** + * Process a log record containing an exception to generate a textual message. + */ +class ExceptionToTextProcessor +{ + public function __invoke(array $record) + { + if (! $this->contextContainsException($record)) { + return $record; + } + + /** @var \Exception $exception */ + $exception = $record['context']['exception']; + + $record['message'] = sprintf( + "%s(%d): %s\n%s", + $exception->getFile(), + $exception->getLine(), + $this->getMessage($exception), + $this->getStackTrace($exception) + ); + + return $record; + } + + private function contextContainsException($record) + { + return isset($record['context']['exception']) + && $record['context']['exception'] instanceof \Exception; + } + + private function getMessage(\Exception $exception) + { + if ($exception instanceof \ErrorException) { + return ErrorHandler::getErrNoString($exception->getSeverity()) . ' - ' . $exception->getMessage(); + } + + return $exception->getMessage(); + } + + private function getStackTrace(\Exception $exception) + { + return Log::$debugBacktraceForTests ?: $exception->getTraceAsString(); + } +} diff --git a/plugins/Monolog/Processor/RequestIdProcessor.php b/plugins/Monolog/Processor/RequestIdProcessor.php new file mode 100644 index 0000000000..f36568193a --- /dev/null +++ b/plugins/Monolog/Processor/RequestIdProcessor.php @@ -0,0 +1,34 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Monolog\Processor; + +use Piwik\Common; + +/** + * Adds a unique "request id" to the log message to follow log entries for each HTTP request. + */ +class RequestIdProcessor +{ + private $currentRequestKey; + + public function __invoke(array $record) + { + if (Common::isPhpCliMode()) { + return $record; + } + + if (empty($this->currentRequestKey)) { + $this->currentRequestKey = substr(Common::generateUniqId(), 0, 5); + } + + $record['extra']['request_id'] = $this->currentRequestKey; + + return $record; + } +} diff --git a/plugins/Monolog/Processor/SprintfProcessor.php b/plugins/Monolog/Processor/SprintfProcessor.php new file mode 100644 index 0000000000..dafa46e5ba --- /dev/null +++ b/plugins/Monolog/Processor/SprintfProcessor.php @@ -0,0 +1,40 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Monolog\Processor; + +/** + * Processes a log message using `sprintf()`. + */ +class SprintfProcessor +{ + public function __invoke(array $record) + { + $message = $record['message']; + $parameters = $record['context']; + + if (is_string($message) && !empty($parameters)) { + $parameters = $this->ensureParametersAreStrings($parameters); + + $record['message'] = vsprintf($message, $parameters); + } + + return $record; + } + + private function ensureParametersAreStrings(array $parameters) + { + foreach ($parameters as &$param) { + if (is_array($param)) { + $param = json_encode($param); + } + } + + return $parameters; + } +} diff --git a/plugins/Monolog/Test/Integration/Fixture/LoggerWrapper.php b/plugins/Monolog/Test/Integration/Fixture/LoggerWrapper.php new file mode 100644 index 0000000000..0d5ea11264 --- /dev/null +++ b/plugins/Monolog/Test/Integration/Fixture/LoggerWrapper.php @@ -0,0 +1,19 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Monolog\Test\Integration\Fixture; + +use Piwik\Log; + +class LoggerWrapper +{ + public static function doLog($message) + { + Log::warning($message); + } +} diff --git a/plugins/Monolog/Test/Integration/LogTest.php b/plugins/Monolog/Test/Integration/LogTest.php new file mode 100644 index 0000000000..c21a2f82f5 --- /dev/null +++ b/plugins/Monolog/Test/Integration/LogTest.php @@ -0,0 +1,237 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Monolog\Test\Integration; + +use Exception; +use Piwik\Common; +use Piwik\Config; +use Piwik\Container\ContainerFactory; +use Piwik\Container\StaticContainer; +use Piwik\Db; +use Piwik\Log; +use Piwik\Plugins\Monolog\Test\Integration\Fixture\LoggerWrapper; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +/** + * @group Core + * @group Log + */ +class LogTest extends IntegrationTestCase +{ + const TESTMESSAGE = 'test%smessage'; + const STRING_MESSAGE_FORMAT = '[%tag%] %message%'; + const STRING_MESSAGE_FORMAT_SPRINTF = "[%s] %s"; + + public static $expectedExceptionOutput = '[Monolog] LogTest.php(120): dummy error message + dummy backtrace'; + + public static $expectedErrorOutput = '[Monolog] dummyerrorfile.php(145): Unknown error (102) - dummy error string + dummy backtrace'; + + public function setUp() + { + parent::setUp(); + + // Create the container in the normal environment (because in tests logging is disabled) + $containerFactory = new ContainerFactory(); + $container = $containerFactory->create(); + StaticContainer::set($container); + Log::unsetInstance(); + + Config::getInstance()->log['string_message_format'] = self::STRING_MESSAGE_FORMAT; + Config::getInstance()->log['logger_file_path'] = self::getLogFileLocation(); + Config::getInstance()->log['log_level'] = Log::INFO; + @unlink(self::getLogFileLocation()); + Log::$debugBacktraceForTests = "dummy backtrace"; + } + + public function tearDown() + { + parent::tearDown(); + + StaticContainer::clearContainer(); + Log::unsetInstance(); + + @unlink(self::getLogFileLocation()); + Log::$debugBacktraceForTests = null; + } + + /** + * Data provider for every test. + */ + public function getBackendsToTest() + { + return array( + 'file' => array('file'), + 'database' => array('database'), + ); + } + + /** + * @dataProvider getBackendsToTest + */ + public function testLoggingWorksWhenMessageIsString($backend) + { + Config::getInstance()->log['log_writers'] = array($backend); + + Log::warning(self::TESTMESSAGE); + + $this->checkBackend($backend, self::TESTMESSAGE, $formatMessage = true, $tag = 'Monolog'); + } + + /** + * @dataProvider getBackendsToTest + */ + public function testLoggingWorksWhenMessageIsSprintfString($backend) + { + Config::getInstance()->log['log_writers'] = array($backend); + + Log::warning(self::TESTMESSAGE, " subst "); + + $this->checkBackend($backend, sprintf(self::TESTMESSAGE, " subst "), $formatMessage = true, $tag = 'Monolog'); + } + + /** + * @dataProvider getBackendsToTest + */ + public function testLoggingWorksWhenMessageIsError($backend) + { + Config::getInstance()->log['log_writers'] = array($backend); + + $error = new \ErrorException("dummy error string", 0, 102, "dummyerrorfile.php", 145); + Log::error($error); + + $this->checkBackend($backend, self::$expectedErrorOutput, $formatMessage = false, $tag = 'Monolog'); + } + + /** + * @dataProvider getBackendsToTest + */ + public function testLoggingWorksWhenMessageIsException($backend) + { + Config::getInstance()->log['log_writers'] = array($backend); + + $exception = new Exception("dummy error message"); + Log::error($exception); + + $this->checkBackend($backend, self::$expectedExceptionOutput, $formatMessage = false, $tag = 'Monolog'); + } + + /** + * @dataProvider getBackendsToTest + */ + public function testLoggingCorrectlyIdentifiesPlugin($backend) + { + Config::getInstance()->log['log_writers'] = array($backend); + + LoggerWrapper::doLog(self::TESTMESSAGE); + + $this->checkBackend($backend, self::TESTMESSAGE, $formatMessage = true, 'Monolog'); + } + + /** + * @dataProvider getBackendsToTest + */ + public function testLogMessagesIgnoredWhenNotWithinLevel($backend) + { + Config::getInstance()->log['log_writers'] = array($backend); + Config::getInstance()->log['log_level'] = 'ERROR'; + + Log::info(self::TESTMESSAGE); + + $this->checkNoMessagesLogged($backend); + } + + /** + * @dataProvider getBackendsToTest + */ + public function testLogMessagesAreTrimmed($backend) + { + Config::getInstance()->log['log_writers'] = array($backend); + + LoggerWrapper::doLog(" \n ".self::TESTMESSAGE."\n\n\n \n"); + + $this->checkBackend($backend, self::TESTMESSAGE, $formatMessage = true, 'Monolog'); + } + + /** + * The database logs requests at DEBUG level, so we check that there is no recursive + * loop (logger insert in databases, which logs the query, ...) + * @link https://github.com/piwik/piwik/issues/7017 + */ + public function testNoInfiniteLoopWhenLoggingToDatabase() + { + Config::getInstance()->log['log_writers'] = array('database'); + Config::getInstance()->log['log_level'] = 'DEBUG'; + + Log::info(self::TESTMESSAGE); + + $this->checkBackend('database', self::TESTMESSAGE, $formatMessage = true, $tag = 'Monolog'); + } + + private function checkBackend($backend, $expectedMessage, $formatMessage = false, $tag = false) + { + if ($formatMessage) { + $expectedMessage = sprintf(self::STRING_MESSAGE_FORMAT_SPRINTF, $tag, $expectedMessage); + } + + if ($backend == 'file') { + $this->assertTrue(file_exists(self::getLogFileLocation())); + + $fileContents = file_get_contents(self::getLogFileLocation()); + $fileContents = $this->removePathsFromBacktrace($fileContents); + + $this->assertEquals($expectedMessage . "\n", $fileContents); + } else if ($backend == 'database') { + $queryLog = Db::isQueryLogEnabled(); + Db::enableQueryLog(false); + + $count = Db::fetchOne("SELECT COUNT(*) FROM " . Common::prefixTable('logger_message')); + $this->assertEquals(1, $count); + + $message = Db::fetchOne("SELECT message FROM " . Common::prefixTable('logger_message') . " LIMIT 1"); + $message = $this->removePathsFromBacktrace($message); + $this->assertEquals($expectedMessage, $message); + + $tagInDb = Db::fetchOne("SELECT tag FROM " . Common::prefixTable('logger_message') . " LIMIT 1"); + if ($tag === false) { + $this->assertEmpty($tagInDb); + } else { + $this->assertEquals($tag, $tagInDb); + } + + Db::enableQueryLog($queryLog); + } + } + + private function checkNoMessagesLogged($backend) + { + if ($backend == 'file') { + $this->assertFalse(file_exists(self::getLogFileLocation())); + } else if ($backend == 'database') { + $this->assertEquals(0, Db::fetchOne("SELECT COUNT(*) FROM " . Common::prefixTable('logger_message'))); + } + } + + private function removePathsFromBacktrace($content) + { + return preg_replace_callback("/(?:\/[^\s(<>]+)*\//", function ($matches) { + if ($matches[0] == '/') { + return '/'; + } else { + return ''; + } + }, $content); + } + + public static function getLogFileLocation() + { + return StaticContainer::get('path.tmp') . '/logs/piwik.test.log'; + } +} diff --git a/plugins/Monolog/Test/Unit/Formatter/LineMessageFormatterTest.php b/plugins/Monolog/Test/Unit/Formatter/LineMessageFormatterTest.php new file mode 100644 index 0000000000..2b53633e6d --- /dev/null +++ b/plugins/Monolog/Test/Unit/Formatter/LineMessageFormatterTest.php @@ -0,0 +1,84 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Monolog\Test\Unit\Formatter; + +use DateTime; +use Piwik\Plugins\Monolog\Formatter\LineMessageFormatter; + +/** + * @group Log + * @covers \Piwik\Plugins\Monolog\Formatter\LineMessageFormatter + */ +class LineMessageFormatterTest extends \PHPUnit_Framework_TestCase +{ + /** + * @test + */ + public function it_should_format_with_placeholders() + { + $formatter = new LineMessageFormatter('%level% %tag% %datetime% %message%'); + + $record = array( + 'message' => 'Hello world', + 'datetime' => DateTime::createFromFormat('U', 0), + 'level_name' => 'ERROR', + 'extra' => array( + 'class' => 'Foo' + ), + ); + + $formatted = "ERROR Foo 1970-01-01 00:00:00 Hello world\n"; + + $this->assertEquals($formatted, $formatter->format($record)); + } + + /** + * @test + */ + public function it_should_insert_request_id_if_defined() + { + $formatter = new LineMessageFormatter('%message%'); + + $record = array( + 'message' => 'Hello world', + 'datetime' => DateTime::createFromFormat('U', 0), + 'level_name' => 'ERROR', + 'extra' => array( + 'request_id' => 'request id' + ), + ); + + $formatted = "[request id] Hello world\n"; + + $this->assertEquals($formatted, $formatter->format($record)); + } + + /** + * @test + */ + public function it_should_indent_multiline_message() + { + $formatter = new LineMessageFormatter('%message%'); + + $record = array( + 'message' => "Hello world\ntest\ntest", + 'datetime' => DateTime::createFromFormat('U', 0), + 'level_name' => 'ERROR', + ); + + $formatted = <<<LOG +Hello world + test + test + +LOG; + + $this->assertEquals($formatted, $formatter->format($record)); + } +} diff --git a/plugins/Monolog/Test/Unit/Processor/ClassNameProcessorTest.php b/plugins/Monolog/Test/Unit/Processor/ClassNameProcessorTest.php new file mode 100644 index 0000000000..b1b307a4d7 --- /dev/null +++ b/plugins/Monolog/Test/Unit/Processor/ClassNameProcessorTest.php @@ -0,0 +1,41 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Monolog\Test\Unit\Processor; + +use Piwik\Plugins\Monolog\Processor\ClassNameProcessor; + +/** + * @group Log + * @covers \Piwik\Plugins\Monolog\Processor\ClassNameProcessor + */ +class ClassNameProcessorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @test + */ + public function it_should_append_classname_to_extra() + { + $processor = new ClassNameProcessor(); + + $result = $processor(array( + 'extra' => array( + 'foo' => 'bar', + ), + )); + + $expected = array( + 'extra' => array( + 'foo' => 'bar', + 'class' => 'Monolog', + ), + ); + + $this->assertEquals($expected, $result); + } +} diff --git a/plugins/Monolog/Test/Unit/Processor/ExceptionToTextProcessorTest.php b/plugins/Monolog/Test/Unit/Processor/ExceptionToTextProcessorTest.php new file mode 100644 index 0000000000..5620325ef5 --- /dev/null +++ b/plugins/Monolog/Test/Unit/Processor/ExceptionToTextProcessorTest.php @@ -0,0 +1,85 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Monolog\Test\Unit\Processor; + +use Piwik\Log; +use Piwik\Plugins\Monolog\Processor\ExceptionToTextProcessor; + +/** + * @group Log + * @covers \Piwik\Plugins\Monolog\Processor\ExceptionToTextProcessor + */ +class ExceptionToTextProcessorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @test + */ + public function it_should_skip_if_no_exception() + { + $processor = new ExceptionToTextProcessor(); + + $record = array('message' => 'Hello world'); + + $this->assertEquals($record, $processor($record)); + } + + /** + * @test + */ + public function it_should_replace_message_with_formatted_exception() + { + $processor = new ExceptionToTextProcessor(); + Log::$debugBacktraceForTests = '[stack trace]'; + + $exception = new \Exception('Hello world'); + $record = array( + 'context' => array( + 'exception' => $exception, + ), + ); + + $result = $processor($record); + + $expected = array( + 'message' => __FILE__ . "(40): Hello world\n[stack trace]", + 'context' => array( + 'exception' => $exception, + ), + ); + + $this->assertEquals($expected, $result); + } + + /** + * @test + */ + public function it_should_add_severity_for_errors() + { + $processor = new ExceptionToTextProcessor(); + Log::$debugBacktraceForTests = '[stack trace]'; + + $exception = new \ErrorException('Hello world', 0, 1, 'file.php', 123); + $record = array( + 'context' => array( + 'exception' => $exception, + ), + ); + + $result = $processor($record); + + $expected = array( + 'message' => "file.php(123): Error - Hello world\n[stack trace]", + 'context' => array( + 'exception' => $exception, + ), + ); + + $this->assertEquals($expected, $result); + } +} diff --git a/plugins/Monolog/Test/Unit/Processor/RequestIdProcessorTest.php b/plugins/Monolog/Test/Unit/Processor/RequestIdProcessorTest.php new file mode 100644 index 0000000000..3fc3c6a2f9 --- /dev/null +++ b/plugins/Monolog/Test/Unit/Processor/RequestIdProcessorTest.php @@ -0,0 +1,62 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Monolog\Test\Unit\Processor; + +use PHPUnit_Framework_TestCase; +use Piwik\Common; +use Piwik\Plugins\Monolog\Processor\RequestIdProcessor; + +/** + * @group Log + * @covers \Piwik\Plugins\Monolog\Processor\RequestIdProcessor + */ +class RequestIdProcessorTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + parent::setUp(); + Common::$isCliMode = false; + } + + public function tearDown() + { + parent::tearDown(); + Common::$isCliMode = true; + } + + /** + * @test + */ + public function it_should_append_request_id_to_extra() + { + $processor = new RequestIdProcessor(); + + $result = $processor(array()); + + $this->assertArrayHasKey('request_id', $result['extra']); + $this->assertInternalType('string', $result['extra']['request_id']); + $this->assertNotEmpty($result['extra']['request_id']); + } + + /** + * @test + */ + public function request_id_should_stay_the_same() + { + $processor = new RequestIdProcessor(); + + $result = $processor(array()); + $id1 = $result['extra']['request_id']; + + $result = $processor(array()); + $id2 = $result['extra']['request_id']; + + $this->assertEquals($id1, $id2); + } +} diff --git a/plugins/Monolog/Test/Unit/Processor/SprintfProcessorTest.php b/plugins/Monolog/Test/Unit/Processor/SprintfProcessorTest.php new file mode 100644 index 0000000000..27ac6cbd02 --- /dev/null +++ b/plugins/Monolog/Test/Unit/Processor/SprintfProcessorTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Monolog\Test\Unit\Processor; + +use Piwik\Plugins\Monolog\Processor\SprintfProcessor; + +/** + * @group Log + * @covers \Piwik\Plugins\Monolog\Processor\SprintfProcessor + */ +class SprintfProcessorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @test + */ + public function it_should_replace_placeholders() + { + $result = $this->process(array( + 'message' => 'Test %s and %s.', + 'context' => array('here', 'there'), + )); + + $this->assertEquals('Test here and there.', $result['message']); + } + + /** + * @test + */ + public function it_should_ignore_strings_without_placeholders() + { + $result = $this->process(array( + 'message' => 'Hello world!', + 'context' => array('foo', 'bar'), + )); + + $this->assertEquals('Hello world!', $result['message']); + } + + /** + * @test + */ + public function it_should_serialize_arrays() + { + $result = $this->process(array( + 'message' => 'Error in the following modules: %s', + 'context' => array(array('import', 'export')), + )); + + $this->assertEquals('Error in the following modules: ["import","export"]', $result['message']); + } + + private function process($record) + { + $processor = new SprintfProcessor(); + return $processor($record); + } +} diff --git a/plugins/Monolog/config/config.php b/plugins/Monolog/config/config.php new file mode 100644 index 0000000000..02aff13313 --- /dev/null +++ b/plugins/Monolog/config/config.php @@ -0,0 +1,99 @@ +<?php + +use Interop\Container\ContainerInterface; +use Monolog\Logger; +use Piwik\Log; + +return array( + + 'Psr\Log\LoggerInterface' => DI\object('Monolog\Logger') + ->constructor('piwik', DI\link('log.handlers'), DI\link('log.processors')), + + 'log.handlers' => DI\factory(function (ContainerInterface $c) { + if ($c->has('ini.log.log_writers')) { + $writerNames = $c->get('ini.log.log_writers'); + } else { + return array(); + } + $classes = array( + 'file' => 'Piwik\Plugins\Monolog\Handler\FileHandler', + 'screen' => 'Piwik\Plugins\Monolog\Handler\WebNotificationHandler', + 'database' => 'Piwik\Plugins\Monolog\Handler\DatabaseHandler', + ); + $writerNames = array_map('trim', $writerNames); + $writers = array(); + foreach ($writerNames as $writerName) { + if (isset($classes[$writerName])) { + $writers[$writerName] = $c->get($classes[$writerName]); + } + } + return array_values($writers); + }), + + 'log.processors' => array( + DI\link('Piwik\Plugins\Monolog\Processor\ClassNameProcessor'), + DI\link('Piwik\Plugins\Monolog\Processor\RequestIdProcessor'), + DI\link('Piwik\Plugins\Monolog\Processor\ExceptionToTextProcessor'), + DI\link('Piwik\Plugins\Monolog\Processor\SprintfProcessor'), + DI\link('Monolog\Processor\PsrLogMessageProcessor'), + ), + + 'Piwik\Plugins\Monolog\Handler\FileHandler' => DI\object() + ->constructor(DI\link('log.file.filename'), DI\link('log.level')) + ->method('setFormatter', DI\link('Piwik\Plugins\Monolog\Formatter\LineMessageFormatter')), + + 'Piwik\Plugins\Monolog\Handler\DatabaseHandler' => DI\object() + ->constructor(DI\link('log.level')) + ->method('setFormatter', DI\link('Piwik\Plugins\Monolog\Formatter\LineMessageFormatter')), + + 'Piwik\Plugins\Monolog\Handler\WebNotificationHandler' => DI\object() + ->constructor(DI\link('log.level')) + ->method('setFormatter', DI\link('Piwik\Plugins\Monolog\Formatter\LineMessageFormatter')), + + 'log.level' => DI\factory(function (ContainerInterface $c) { + if ($c->has('ini.log.log_level')) { + $level = strtoupper($c->get('ini.log.log_level')); + if (!empty($level) && defined('Piwik\Log::'.strtoupper($level))) { + return Log::getMonologLevel(constant('Piwik\Log::'.strtoupper($level))); + } + } + return Logger::WARNING; + }), + + 'log.file.filename' => DI\factory(function (ContainerInterface $c) { + $logPath = $c->get('ini.log.logger_file_path'); + + // Absolute path + if (strpos($logPath, '/') === 0) { + return $logPath; + } + + // Remove 'tmp/' at the beginning + if (strpos($logPath, 'tmp/') === 0) { + $logPath = substr($logPath, strlen('tmp')); + } + + if (empty($logPath)) { + // Default log file + $logPath = '/logs/piwik.log'; + } + + $logPath = $c->get('path.tmp') . $logPath; + if (is_dir($logPath)) { + $logPath .= '/piwik.log'; + } + + return $logPath; + }), + + 'Piwik\Plugins\Monolog\Formatter\LineMessageFormatter' => DI\object() + ->constructor(DI\link('log.format')), + + 'log.format' => DI\factory(function (ContainerInterface $c) { + if ($c->has('ini.log.string_message_format')) { + return $c->get('ini.log.string_message_format'); + } + return '%level% %tag%[%datetime%] %message%'; + }), + +); |