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