1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\Log; 11 12use DateTime; 13use ErrorException; 14use Traversable; 15use Zend\ServiceManager\AbstractPluginManager; 16use Zend\Stdlib\ArrayUtils; 17use Zend\Stdlib\SplPriorityQueue; 18 19/** 20 * Logging messages with a stack of backends 21 */ 22class Logger implements LoggerInterface 23{ 24 /** 25 * @const int defined from the BSD Syslog message severities 26 * @link http://tools.ietf.org/html/rfc3164 27 */ 28 const EMERG = 0; 29 const ALERT = 1; 30 const CRIT = 2; 31 const ERR = 3; 32 const WARN = 4; 33 const NOTICE = 5; 34 const INFO = 6; 35 const DEBUG = 7; 36 37 /** 38 * Map native PHP errors to priority 39 * 40 * @var array 41 */ 42 public static $errorPriorityMap = array( 43 E_NOTICE => self::NOTICE, 44 E_USER_NOTICE => self::NOTICE, 45 E_WARNING => self::WARN, 46 E_CORE_WARNING => self::WARN, 47 E_USER_WARNING => self::WARN, 48 E_ERROR => self::ERR, 49 E_USER_ERROR => self::ERR, 50 E_CORE_ERROR => self::ERR, 51 E_RECOVERABLE_ERROR => self::ERR, 52 E_PARSE => self::ERR, 53 E_COMPILE_ERROR => self::ERR, 54 E_COMPILE_WARNING => self::ERR, 55 E_STRICT => self::DEBUG, 56 E_DEPRECATED => self::DEBUG, 57 E_USER_DEPRECATED => self::DEBUG, 58 ); 59 60 /** 61 * Registered error handler 62 * 63 * @var bool 64 */ 65 protected static $registeredErrorHandler = false; 66 67 /** 68 * Registered shutdown error handler 69 * 70 * @var bool 71 */ 72 protected static $registeredFatalErrorShutdownFunction = false; 73 74 /** 75 * Registered exception handler 76 * 77 * @var bool 78 */ 79 protected static $registeredExceptionHandler = false; 80 81 /** 82 * List of priority code => priority (short) name 83 * 84 * @var array 85 */ 86 protected $priorities = array( 87 self::EMERG => 'EMERG', 88 self::ALERT => 'ALERT', 89 self::CRIT => 'CRIT', 90 self::ERR => 'ERR', 91 self::WARN => 'WARN', 92 self::NOTICE => 'NOTICE', 93 self::INFO => 'INFO', 94 self::DEBUG => 'DEBUG', 95 ); 96 97 /** 98 * Writers 99 * 100 * @var SplPriorityQueue 101 */ 102 protected $writers; 103 104 /** 105 * Processors 106 * 107 * @var SplPriorityQueue 108 */ 109 protected $processors; 110 111 /** 112 * Writer plugins 113 * 114 * @var WriterPluginManager 115 */ 116 protected $writerPlugins; 117 118 /** 119 * Processor plugins 120 * 121 * @var ProcessorPluginManager 122 */ 123 protected $processorPlugins; 124 125 /** 126 * Constructor 127 * 128 * Set options for a logger. Accepted options are: 129 * - writers: array of writers to add to this logger 130 * - exceptionhandler: if true register this logger as exceptionhandler 131 * - errorhandler: if true register this logger as errorhandler 132 * 133 * @param array|Traversable $options 134 * @return Logger 135 * @throws Exception\InvalidArgumentException 136 */ 137 public function __construct($options = null) 138 { 139 $this->writers = new SplPriorityQueue(); 140 $this->processors = new SplPriorityQueue(); 141 142 if ($options instanceof Traversable) { 143 $options = ArrayUtils::iteratorToArray($options); 144 } 145 146 if (!$options) { 147 return; 148 } 149 150 if (!is_array($options)) { 151 throw new Exception\InvalidArgumentException('Options must be an array or an object implementing \Traversable '); 152 } 153 154 // Inject writer plugin manager, if available 155 if (isset($options['writer_plugin_manager']) 156 && $options['writer_plugin_manager'] instanceof AbstractPluginManager 157 ) { 158 $this->setWriterPluginManager($options['writer_plugin_manager']); 159 } 160 161 // Inject processor plugin manager, if available 162 if (isset($options['processor_plugin_manager']) 163 && $options['processor_plugin_manager'] instanceof AbstractPluginManager 164 ) { 165 $this->setProcessorPluginManager($options['processor_plugin_manager']); 166 } 167 168 if (isset($options['writers']) && is_array($options['writers'])) { 169 foreach ($options['writers'] as $writer) { 170 if (!isset($writer['name'])) { 171 throw new Exception\InvalidArgumentException('Options must contain a name for the writer'); 172 } 173 174 $priority = (isset($writer['priority'])) ? $writer['priority'] : null; 175 $writerOptions = (isset($writer['options'])) ? $writer['options'] : null; 176 177 $this->addWriter($writer['name'], $priority, $writerOptions); 178 } 179 } 180 181 if (isset($options['processors']) && is_array($options['processors'])) { 182 foreach ($options['processors'] as $processor) { 183 if (!isset($processor['name'])) { 184 throw new Exception\InvalidArgumentException('Options must contain a name for the processor'); 185 } 186 187 $priority = (isset($processor['priority'])) ? $processor['priority'] : null; 188 $processorOptions = (isset($processor['options'])) ? $processor['options'] : null; 189 190 $this->addProcessor($processor['name'], $priority, $processorOptions); 191 } 192 } 193 194 if (isset($options['exceptionhandler']) && $options['exceptionhandler'] === true) { 195 static::registerExceptionHandler($this); 196 } 197 198 if (isset($options['errorhandler']) && $options['errorhandler'] === true) { 199 static::registerErrorHandler($this); 200 } 201 202 if (isset($options['fatal_error_shutdownfunction']) && $options['fatal_error_shutdownfunction'] === true) { 203 static::registerFatalErrorShutdownFunction($this); 204 } 205 } 206 207 /** 208 * Shutdown all writers 209 * 210 * @return void 211 */ 212 public function __destruct() 213 { 214 foreach ($this->writers as $writer) { 215 try { 216 $writer->shutdown(); 217 } catch (\Exception $e) { 218 } 219 } 220 } 221 222 /** 223 * Get writer plugin manager 224 * 225 * @return WriterPluginManager 226 */ 227 public function getWriterPluginManager() 228 { 229 if (null === $this->writerPlugins) { 230 $this->setWriterPluginManager(new WriterPluginManager()); 231 } 232 return $this->writerPlugins; 233 } 234 235 /** 236 * Set writer plugin manager 237 * 238 * @param string|WriterPluginManager $plugins 239 * @return Logger 240 * @throws Exception\InvalidArgumentException 241 */ 242 public function setWriterPluginManager($plugins) 243 { 244 if (is_string($plugins)) { 245 $plugins = new $plugins; 246 } 247 if (!$plugins instanceof WriterPluginManager) { 248 throw new Exception\InvalidArgumentException(sprintf( 249 'Writer plugin manager must extend %s\WriterPluginManager; received %s', 250 __NAMESPACE__, 251 is_object($plugins) ? get_class($plugins) : gettype($plugins) 252 )); 253 } 254 255 $this->writerPlugins = $plugins; 256 return $this; 257 } 258 259 /** 260 * Get writer instance 261 * 262 * @param string $name 263 * @param array|null $options 264 * @return Writer\WriterInterface 265 */ 266 public function writerPlugin($name, array $options = null) 267 { 268 return $this->getWriterPluginManager()->get($name, $options); 269 } 270 271 /** 272 * Add a writer to a logger 273 * 274 * @param string|Writer\WriterInterface $writer 275 * @param int $priority 276 * @param array|null $options 277 * @return Logger 278 * @throws Exception\InvalidArgumentException 279 */ 280 public function addWriter($writer, $priority = 1, array $options = null) 281 { 282 if (is_string($writer)) { 283 $writer = $this->writerPlugin($writer, $options); 284 } elseif (!$writer instanceof Writer\WriterInterface) { 285 throw new Exception\InvalidArgumentException(sprintf( 286 'Writer must implement %s\Writer\WriterInterface; received "%s"', 287 __NAMESPACE__, 288 is_object($writer) ? get_class($writer) : gettype($writer) 289 )); 290 } 291 $this->writers->insert($writer, $priority); 292 293 return $this; 294 } 295 296 /** 297 * Get writers 298 * 299 * @return SplPriorityQueue 300 */ 301 public function getWriters() 302 { 303 return $this->writers; 304 } 305 306 /** 307 * Set the writers 308 * 309 * @param SplPriorityQueue $writers 310 * @return Logger 311 * @throws Exception\InvalidArgumentException 312 */ 313 public function setWriters(SplPriorityQueue $writers) 314 { 315 foreach ($writers->toArray() as $writer) { 316 if (!$writer instanceof Writer\WriterInterface) { 317 throw new Exception\InvalidArgumentException('Writers must be a SplPriorityQueue of Zend\Log\Writer'); 318 } 319 } 320 $this->writers = $writers; 321 return $this; 322 } 323 324 /** 325 * Get processor plugin manager 326 * 327 * @return ProcessorPluginManager 328 */ 329 public function getProcessorPluginManager() 330 { 331 if (null === $this->processorPlugins) { 332 $this->setProcessorPluginManager(new ProcessorPluginManager()); 333 } 334 return $this->processorPlugins; 335 } 336 337 /** 338 * Set processor plugin manager 339 * 340 * @param string|ProcessorPluginManager $plugins 341 * @return Logger 342 * @throws Exception\InvalidArgumentException 343 */ 344 public function setProcessorPluginManager($plugins) 345 { 346 if (is_string($plugins)) { 347 $plugins = new $plugins; 348 } 349 if (!$plugins instanceof ProcessorPluginManager) { 350 throw new Exception\InvalidArgumentException(sprintf( 351 'processor plugin manager must extend %s\ProcessorPluginManager; received %s', 352 __NAMESPACE__, 353 is_object($plugins) ? get_class($plugins) : gettype($plugins) 354 )); 355 } 356 357 $this->processorPlugins = $plugins; 358 return $this; 359 } 360 361 /** 362 * Get processor instance 363 * 364 * @param string $name 365 * @param array|null $options 366 * @return Processor\ProcessorInterface 367 */ 368 public function processorPlugin($name, array $options = null) 369 { 370 return $this->getProcessorPluginManager()->get($name, $options); 371 } 372 373 /** 374 * Add a processor to a logger 375 * 376 * @param string|Processor\ProcessorInterface $processor 377 * @param int $priority 378 * @param array|null $options 379 * @return Logger 380 * @throws Exception\InvalidArgumentException 381 */ 382 public function addProcessor($processor, $priority = 1, array $options = null) 383 { 384 if (is_string($processor)) { 385 $processor = $this->processorPlugin($processor, $options); 386 } elseif (!$processor instanceof Processor\ProcessorInterface) { 387 throw new Exception\InvalidArgumentException(sprintf( 388 'Processor must implement Zend\Log\ProcessorInterface; received "%s"', 389 is_object($processor) ? get_class($processor) : gettype($processor) 390 )); 391 } 392 $this->processors->insert($processor, $priority); 393 394 return $this; 395 } 396 397 /** 398 * Get processors 399 * 400 * @return SplPriorityQueue 401 */ 402 public function getProcessors() 403 { 404 return $this->processors; 405 } 406 407 /** 408 * Add a message as a log entry 409 * 410 * @param int $priority 411 * @param mixed $message 412 * @param array|Traversable $extra 413 * @return Logger 414 * @throws Exception\InvalidArgumentException if message can't be cast to string 415 * @throws Exception\InvalidArgumentException if extra can't be iterated over 416 * @throws Exception\RuntimeException if no log writer specified 417 */ 418 public function log($priority, $message, $extra = array()) 419 { 420 if (!is_int($priority) || ($priority<0) || ($priority>=count($this->priorities))) { 421 throw new Exception\InvalidArgumentException(sprintf( 422 '$priority must be an integer >= 0 and < %d; received %s', 423 count($this->priorities), 424 var_export($priority, 1) 425 )); 426 } 427 if (is_object($message) && !method_exists($message, '__toString')) { 428 throw new Exception\InvalidArgumentException( 429 '$message must implement magic __toString() method' 430 ); 431 } 432 433 if (!is_array($extra) && !$extra instanceof Traversable) { 434 throw new Exception\InvalidArgumentException( 435 '$extra must be an array or implement Traversable' 436 ); 437 } elseif ($extra instanceof Traversable) { 438 $extra = ArrayUtils::iteratorToArray($extra); 439 } 440 441 if ($this->writers->count() === 0) { 442 throw new Exception\RuntimeException('No log writer specified'); 443 } 444 445 $timestamp = new DateTime(); 446 447 if (is_array($message)) { 448 $message = var_export($message, true); 449 } 450 451 $event = array( 452 'timestamp' => $timestamp, 453 'priority' => (int) $priority, 454 'priorityName' => $this->priorities[$priority], 455 'message' => (string) $message, 456 'extra' => $extra, 457 ); 458 459 foreach ($this->processors->toArray() as $processor) { 460 $event = $processor->process($event); 461 } 462 463 foreach ($this->writers->toArray() as $writer) { 464 $writer->write($event); 465 } 466 467 return $this; 468 } 469 470 /** 471 * @param string $message 472 * @param array|Traversable $extra 473 * @return Logger 474 */ 475 public function emerg($message, $extra = array()) 476 { 477 return $this->log(self::EMERG, $message, $extra); 478 } 479 480 /** 481 * @param string $message 482 * @param array|Traversable $extra 483 * @return Logger 484 */ 485 public function alert($message, $extra = array()) 486 { 487 return $this->log(self::ALERT, $message, $extra); 488 } 489 490 /** 491 * @param string $message 492 * @param array|Traversable $extra 493 * @return Logger 494 */ 495 public function crit($message, $extra = array()) 496 { 497 return $this->log(self::CRIT, $message, $extra); 498 } 499 500 /** 501 * @param string $message 502 * @param array|Traversable $extra 503 * @return Logger 504 */ 505 public function err($message, $extra = array()) 506 { 507 return $this->log(self::ERR, $message, $extra); 508 } 509 510 /** 511 * @param string $message 512 * @param array|Traversable $extra 513 * @return Logger 514 */ 515 public function warn($message, $extra = array()) 516 { 517 return $this->log(self::WARN, $message, $extra); 518 } 519 520 /** 521 * @param string $message 522 * @param array|Traversable $extra 523 * @return Logger 524 */ 525 public function notice($message, $extra = array()) 526 { 527 return $this->log(self::NOTICE, $message, $extra); 528 } 529 530 /** 531 * @param string $message 532 * @param array|Traversable $extra 533 * @return Logger 534 */ 535 public function info($message, $extra = array()) 536 { 537 return $this->log(self::INFO, $message, $extra); 538 } 539 540 /** 541 * @param string $message 542 * @param array|Traversable $extra 543 * @return Logger 544 */ 545 public function debug($message, $extra = array()) 546 { 547 return $this->log(self::DEBUG, $message, $extra); 548 } 549 550 /** 551 * Register logging system as an error handler to log PHP errors 552 * 553 * @link http://www.php.net/manual/function.set-error-handler.php 554 * @param Logger $logger 555 * @param bool $continueNativeHandler 556 * @return mixed Returns result of set_error_handler 557 * @throws Exception\InvalidArgumentException if logger is null 558 */ 559 public static function registerErrorHandler(Logger $logger, $continueNativeHandler = false) 560 { 561 // Only register once per instance 562 if (static::$registeredErrorHandler) { 563 return false; 564 } 565 566 $errorPriorityMap = static::$errorPriorityMap; 567 568 $previous = set_error_handler(function ($level, $message, $file, $line) use ($logger, $errorPriorityMap, $continueNativeHandler) { 569 $iniLevel = error_reporting(); 570 571 if ($iniLevel & $level) { 572 if (isset($errorPriorityMap[$level])) { 573 $priority = $errorPriorityMap[$level]; 574 } else { 575 $priority = Logger::INFO; 576 } 577 $logger->log($priority, $message, array( 578 'errno' => $level, 579 'file' => $file, 580 'line' => $line, 581 )); 582 } 583 584 return !$continueNativeHandler; 585 }); 586 587 static::$registeredErrorHandler = true; 588 return $previous; 589 } 590 591 /** 592 * Unregister error handler 593 * 594 */ 595 public static function unregisterErrorHandler() 596 { 597 restore_error_handler(); 598 static::$registeredErrorHandler = false; 599 } 600 601 /** 602 * Register a shutdown handler to log fatal errors 603 * 604 * @link http://www.php.net/manual/function.register-shutdown-function.php 605 * @param Logger $logger 606 * @return bool 607 */ 608 public static function registerFatalErrorShutdownFunction(Logger $logger) 609 { 610 // Only register once per instance 611 if (static::$registeredFatalErrorShutdownFunction) { 612 return false; 613 } 614 615 $errorPriorityMap = static::$errorPriorityMap; 616 617 register_shutdown_function(function () use ($logger, $errorPriorityMap) { 618 $error = error_get_last(); 619 620 if (null === $error 621 || ! in_array( 622 $error['type'], 623 array( 624 E_ERROR, 625 E_PARSE, 626 E_CORE_ERROR, 627 E_CORE_WARNING, 628 E_COMPILE_ERROR, 629 E_COMPILE_WARNING 630 ), 631 true 632 ) 633 ) { 634 return; 635 } 636 637 $logger->log($errorPriorityMap[$error['type']], 638 $error['message'], 639 array( 640 'file' => $error['file'], 641 'line' => $error['line'], 642 ) 643 ); 644 }); 645 646 static::$registeredFatalErrorShutdownFunction = true; 647 648 return true; 649 } 650 651 /** 652 * Register logging system as an exception handler to log PHP exceptions 653 * 654 * @link http://www.php.net/manual/en/function.set-exception-handler.php 655 * @param Logger $logger 656 * @return bool 657 * @throws Exception\InvalidArgumentException if logger is null 658 */ 659 public static function registerExceptionHandler(Logger $logger) 660 { 661 // Only register once per instance 662 if (static::$registeredExceptionHandler) { 663 return false; 664 } 665 666 if ($logger === null) { 667 throw new Exception\InvalidArgumentException('Invalid Logger specified'); 668 } 669 670 $errorPriorityMap = static::$errorPriorityMap; 671 672 set_exception_handler(function ($exception) use ($logger, $errorPriorityMap) { 673 $logMessages = array(); 674 675 do { 676 $priority = Logger::ERR; 677 if ($exception instanceof ErrorException && isset($errorPriorityMap[$exception->getSeverity()])) { 678 $priority = $errorPriorityMap[$exception->getSeverity()]; 679 } 680 681 $extra = array( 682 'file' => $exception->getFile(), 683 'line' => $exception->getLine(), 684 'trace' => $exception->getTrace(), 685 ); 686 if (isset($exception->xdebug_message)) { 687 $extra['xdebug'] = $exception->xdebug_message; 688 } 689 690 $logMessages[] = array( 691 'priority' => $priority, 692 'message' => $exception->getMessage(), 693 'extra' => $extra, 694 ); 695 $exception = $exception->getPrevious(); 696 } while ($exception); 697 698 foreach (array_reverse($logMessages) as $logMessage) { 699 $logger->log($logMessage['priority'], $logMessage['message'], $logMessage['extra']); 700 } 701 }); 702 703 static::$registeredExceptionHandler = true; 704 return true; 705 } 706 707 /** 708 * Unregister exception handler 709 */ 710 public static function unregisterExceptionHandler() 711 { 712 restore_exception_handler(); 713 static::$registeredExceptionHandler = false; 714 } 715} 716