From cd4e6b3b2697d09f210e9f003fce2da0c9b1b6e5 Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Thu, 27 Sep 2012 17:59:32 -0700 Subject: [PATCH] Initial logging proposal. C.f. #112 Add logging to the Mustache Engine. * Add a Logger interface. * If a Logger instance is passed to the Engine constructor (or added later via setLogger) template compiling, caching, errors and missing partials will be logged. * Add a Stream Logger and Monolog Logger implementation. You should use the Monolog Logger. --- src/Mustache/Engine.php | 96 ++++++++++++++++++++++++- src/Mustache/Logger.php | 75 ++++++++++++++++++++ src/Mustache/Logger/AbstractLogger.php | 98 ++++++++++++++++++++++++++ src/Mustache/Logger/MonologLogger.php | 59 ++++++++++++++++ src/Mustache/Logger/StreamLogger.php | 63 +++++++++++++++++ 5 files changed, 388 insertions(+), 3 deletions(-) create mode 100644 src/Mustache/Logger.php create mode 100644 src/Mustache/Logger/AbstractLogger.php create mode 100644 src/Mustache/Logger/MonologLogger.php create mode 100644 src/Mustache/Logger/StreamLogger.php diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index 34d3ce2..b5b7b22 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -34,12 +34,13 @@ class Mustache_Engine // Environment private $templateClassPrefix = '__Mustache_'; private $cache = null; + private $cacheFileMode = null; private $loader; private $partialsLoader; private $helpers; private $escape; private $charset = 'UTF-8'; - private $cacheFileMode = null; + private $logger; /** * Mustache class constructor. @@ -81,6 +82,9 @@ class Mustache_Engine * * // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'. * 'charset' => 'ISO-8859-1', + * + * // A Mustache Logger instance. No logging will occur unless this is set. + * 'logger' => new Mustache_StreamLogger('php://stderr'), * ); * * @param array $options (default: array()) @@ -126,6 +130,10 @@ class Mustache_Engine if (isset($options['charset'])) { $this->charset = $options['charset']; } + + if (isset($options['logger'])) { + $this->setLogger($options['logger']); + } } /** @@ -330,6 +338,26 @@ class Mustache_Engine $this->getHelpers()->remove($name); } + /** + * Set the Mustache Logger instance. + * + * @param Mustache_Logger $logger + */ + public function setLogger(Mustache_Logger $logger) + { + $this->logger = $logger; + } + + /** + * Get the current Mustache Logger instance. + * + * @return Mustache_Logger + */ + public function getLogger() + { + return $this->logger; + } + /** * Set the Mustache Tokenizer instance. * @@ -453,7 +481,12 @@ class Mustache_Engine try { return $this->loadSource($this->getPartialsLoader()->load($name)); } catch (InvalidArgumentException $e) { - // If the named partial cannot be found, return null. + // If the named partial cannot be found, log then return null. + $this->log( + Mustache_Logger::WARNING, + sprintf('Partial not found: "%s"', $name), + array('name' => $name) + ); } } @@ -496,15 +529,33 @@ class Mustache_Engine if (!class_exists($className, false)) { if ($fileName = $this->getCacheFilename($source)) { if (!is_file($fileName)) { + $this->log( + Mustache_Logger::DEBUG, + sprintf('Writing "%s" class to template cache: "%s"', $className, $fileName), + array('className' => $className, 'fileName' => $fileName) + ); + $this->writeCacheFile($fileName, $this->compile($source)); } require_once $fileName; } else { + $this->log( + Mustache_Logger::WARNING, + sprintf('Template cache disabled, evaluating "%s" class at runtime', $className), + array('className' => $className) + ); + eval('?>'.$this->compile($source)); } } + $this->log( + Mustache_Logger::DEBUG, + sprintf('Instantiating template: "%s"', $className), + array('className' => $className) + ); + $this->templates[$className] = new $className($this); } @@ -553,6 +604,12 @@ class Mustache_Engine $tree = $this->parse($source); $name = $this->getTemplateClassName($source); + $this->log( + Mustache_Logger::INFO, + sprintf('Compiling template to "%s" class', $name), + array('name' => $name) + ); + return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset); } @@ -573,7 +630,7 @@ class Mustache_Engine /** * Helper method to dump a generated Mustache Template subclass to the file cache. * - * @throws RuntimeException if unable to create the cache directory or write $fileName + * @throws RuntimeException if unable to create the cache directory or write to $fileName. * * @param string $fileName * @param string $source @@ -584,12 +641,25 @@ class Mustache_Engine { $dirName = dirname($fileName); if (!is_dir($dirName)) { + $this->log( + Mustache_Logger::INFO, + sprintf('Creating Mustache template cache directory: "%s"', $dirName), + array('dirName' => $dirName) + ); + @mkdir($dirName, 0777, true); if (!is_dir($dirName)) { throw new RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName)); } + } + $this->log( + Mustache_Logger::DEBUG, + sprintf('Caching compiled template to "%s"', dirname($fileName)), + array('filename' => $fileName) + ); + $tempFile = tempnam($dirName, basename($fileName)); if (false !== @file_put_contents($tempFile, $source)) { if (@rename($tempFile, $fileName)) { @@ -598,8 +668,28 @@ class Mustache_Engine return; } + + $this->log( + Mustache_Logger::ERROR, + sprintf('Unable to rename Mustache temp cache file: "%s" -> "%s"', $tempFile, $fileName), + array('tempFile' => $tempFile, 'fileName' => $fileName) + ); } throw new RuntimeException(sprintf('Failed to write cache file "%s".', $fileName)); } + + /** + * Add a log record if logging is enabled. + * + * @param integer $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + private function log($level, $message, array $context = array()) + { + if (isset($this->logger)) { + $this->logger->log($level, $message, $context); + } + } } diff --git a/src/Mustache/Logger.php b/src/Mustache/Logger.php new file mode 100644 index 0000000..0200970 --- /dev/null +++ b/src/Mustache/Logger.php @@ -0,0 +1,75 @@ + 'DEBUG', + 200 => 'INFO', + 250 => 'NOTICE', + 300 => 'WARNING', + 400 => 'ERROR', + 500 => 'CRITICAL', + 550 => 'ALERT', + 600 => 'EMERGENCY', + ); + + /** + * Abstract Logger constructor. + * + * @throws InvalidArgumentException if the logging level is unknown. + * + * @param integer $level The minimum logging level which will be written + */ + public function __construct($level = self::ERROR) + { + if (!array_key_exists($level, self::$levels)) { + throw new InvalidArgumentException('Unexpected logging level: ' . $level); + } + + $this->level = $level; + } + + /** + * Adds a log record. + * + * @see Mustache_Logger_AbstractLogger::write + * + * @param integer $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + public function log($level, $message, array $context = array()) + { + if ($level >= $this->level) { + $this->writeLog($level, $message, $context); + } + } + + /** + * Gets the name of the logging level. + * + * @throws InvalidArgumentException if the logging level is unknown. + * + * @param integer $level + * + * @return string + */ + public static function getLevelName($level) + { + if (!array_key_exists($level, self::$levels)) { + throw new InvalidArgumentException('Unexpected logging level: ' . $level); + } + + return self::$levels[$level]; + } + + /** + * Format a log line for output. + * + * @param integer $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + public static function formatLine($level, $message, array $context = array()) + { + return sprintf('%s: %s %s', self::getLevelName($level), (string) $message, json_encode($context)); + } + + /** + * Write a record to the log. Implemented by subclasses. + * + * @param integer $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + abstract protected function write($level, $message, array $context = array()); +} diff --git a/src/Mustache/Logger/MonologLogger.php b/src/Mustache/Logger/MonologLogger.php new file mode 100644 index 0000000..2123343 --- /dev/null +++ b/src/Mustache/Logger/MonologLogger.php @@ -0,0 +1,59 @@ +logger = $logger; + } + + /** + * Adds a log record. + * + * Overload the AbstractLogger::log method, because all log messages should + * be passed through to Monolog regardless of the log level. Monolog will + * handle ignoring the messages it doesn't care about. + * + * @param integer $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + public function log($level, $message, array $context = array()) + { + $this->write($level, $message, $context); + } + + /** + * Write a record to the log. + * + * @param integer $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + protected function write($level, $message, array $context = array()) + { + $this->logger->addRecord($level, $message, $context); + } +} diff --git a/src/Mustache/Logger/StreamLogger.php b/src/Mustache/Logger/StreamLogger.php new file mode 100644 index 0000000..05939bf --- /dev/null +++ b/src/Mustache/Logger/StreamLogger.php @@ -0,0 +1,63 @@ +stream = $stream; + } else { + $this->url = $stream; + } + } + + /** + * Write a record to the log. + * + * @param integer $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + protected function write($level, $message, array $context = array()) + { + if ($this->stream === null) { + if (!isset($this->url)) { + throw new LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().'); + } + + $this->stream = fopen($this->url, 'a'); + if (!is_resource($this->stream)) { + throw new UnexpectedValueException(sprintf('The stream or file "%s" could not be opened.', $this->url)); + } + } + + fwrite($this->stream, self::formatLine($level, $message, $context)); + } +}