Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthieu Napoli <matthieu@mnapoli.fr>2015-02-02 01:45:32 +0300
committerMatthieu Napoli <matthieu@mnapoli.fr>2015-02-12 00:12:19 +0300
commite122a4e37b3eef21fc6fc32ed7d31c56c38d673f (patch)
tree89aa9600a70200f1e94cf0454d62d037f0e361e2 /plugins/Monolog
parent47c0eae3f8202430ea576e09cf0ded90f783eea1 (diff)
Moved the monolog configuration and code into a Monolog plugin
Diffstat (limited to 'plugins/Monolog')
-rw-r--r--plugins/Monolog/Formatter/LineMessageFormatter.php75
-rw-r--r--plugins/Monolog/Handler/DatabaseHandler.php39
-rw-r--r--plugins/Monolog/Handler/FileHandler.php31
-rw-r--r--plugins/Monolog/Handler/WebNotificationHandler.php51
-rw-r--r--plugins/Monolog/Monolog.php15
-rw-r--r--plugins/Monolog/Processor/ClassNameProcessor.php81
-rw-r--r--plugins/Monolog/Processor/ExceptionToTextProcessor.php58
-rw-r--r--plugins/Monolog/Processor/RequestIdProcessor.php34
-rw-r--r--plugins/Monolog/Processor/SprintfProcessor.php40
-rw-r--r--plugins/Monolog/Test/Integration/Fixture/LoggerWrapper.php19
-rw-r--r--plugins/Monolog/Test/Integration/LogTest.php237
-rw-r--r--plugins/Monolog/Test/Unit/Formatter/LineMessageFormatterTest.php84
-rw-r--r--plugins/Monolog/Test/Unit/Processor/ClassNameProcessorTest.php41
-rw-r--r--plugins/Monolog/Test/Unit/Processor/ExceptionToTextProcessorTest.php85
-rw-r--r--plugins/Monolog/Test/Unit/Processor/RequestIdProcessorTest.php62
-rw-r--r--plugins/Monolog/Test/Unit/Processor/SprintfProcessorTest.php63
-rw-r--r--plugins/Monolog/config/config.php99
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%';
+ }),
+
+);