1<?php
2/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
3
4namespace Icinga\Application;
5
6use Exception;
7use Icinga\Data\ConfigObject;
8use Icinga\Application\Logger\Writer\FileWriter;
9use Icinga\Application\Logger\Writer\SyslogWriter;
10use Icinga\Exception\ConfigurationError;
11use Icinga\Exception\IcingaException;
12use Icinga\Util\Json;
13
14/**
15 * Logger
16 */
17class Logger
18{
19    /**
20     * Debug message
21     */
22    const DEBUG = 1;
23
24    /**
25     * Informational message
26     */
27    const INFO = 2;
28
29    /**
30     * Warning message
31     */
32    const WARNING = 4;
33
34    /**
35     * Error message
36     */
37    const ERROR = 8;
38
39    /**
40     * Log levels
41     *
42     * @var array
43     */
44    public static $levels = array(
45        Logger::DEBUG   => 'DEBUG',
46        Logger::INFO    => 'INFO',
47        Logger::WARNING => 'WARNING',
48        Logger::ERROR   => 'ERROR'
49    );
50
51    /**
52     * This logger's instance
53     *
54     * @var static
55     */
56    protected static $instance;
57
58    /**
59     * Log writer
60     *
61     * @var \Icinga\Application\Logger\LogWriter
62     */
63    protected $writer;
64
65    /**
66     * Maximum level to emit
67     *
68     * @var int
69     */
70    protected $level;
71
72    /**
73     * Error messages to be displayed prior to any other log message
74     *
75     * @var array
76     */
77    protected $configErrors = array();
78
79    /**
80     * Create a new logger object
81     *
82     * @param   ConfigObject  $config
83     *
84     * @throws  ConfigurationError  If the logging configuration directive 'log' is missing or if the logging level is
85     *                              not defined
86     */
87    public function __construct(ConfigObject $config)
88    {
89        if ($config->log === null) {
90            throw new ConfigurationError('Required logging configuration directive \'log\' missing');
91        }
92
93        $this->setLevel($config->get('level', static::ERROR));
94
95        if (strtolower($config->get('log', 'syslog')) !== 'none') {
96            $this->writer = $this->createWriter($config);
97        }
98    }
99
100    /**
101     * Set the logging level to use
102     *
103     * @param   mixed   $level
104     *
105     * @return  $this
106     *
107     * @throws  ConfigurationError      In case the given level is invalid
108     */
109    public function setLevel($level)
110    {
111        if (is_numeric($level)) {
112            $level = (int) $level;
113            if (! isset(static::$levels[$level])) {
114                throw new ConfigurationError(
115                    'Can\'t set logging level %d. Logging level is invalid. Use one of %s or one of the'
116                    . ' Logger\'s constants.',
117                    $level,
118                    implode(', ', array_keys(static::$levels))
119                );
120            }
121
122            $this->level = $level;
123        } else {
124            $level = strtoupper($level);
125            $levels = array_flip(static::$levels);
126            if (! isset($levels[$level])) {
127                throw new ConfigurationError(
128                    'Can\'t set logging level "%s". Logging level is invalid. Use one of %s.',
129                    $level,
130                    implode(', ', array_keys($levels))
131                );
132            }
133
134            $this->level = $levels[$level];
135        }
136
137        return $this;
138    }
139
140    /**
141     * Return the logging level being used
142     *
143     * @return  int
144     */
145    public function getLevel()
146    {
147        return $this->level;
148    }
149
150    /**
151     * Register the given message as config error
152     *
153     * Config errors are logged every time a log message is being logged.
154     *
155     * @param   mixed   $arg,...    A string, exception or format-string + substitutions
156     *
157     * @return  $this
158     */
159    public function registerConfigError()
160    {
161        if (func_num_args() > 0) {
162            $this->configErrors[] = static::formatMessage(func_get_args());
163        }
164
165        return $this;
166    }
167
168    /**
169     * Create a new logger object
170     *
171     * @param   ConfigObject     $config
172     *
173     * @return  static
174     */
175    public static function create(ConfigObject $config)
176    {
177        static::$instance = new static($config);
178        return static::$instance;
179    }
180
181    /**
182     * Create a log writer
183     *
184     * @param   ConfigObject     $config     The configuration to initialize the writer with
185     *
186     * @return  \Icinga\Application\Logger\LogWriter    The requested log writer
187     * @throws  ConfigurationError                      If the requested writer cannot be found
188     */
189    protected function createWriter(ConfigObject $config)
190    {
191        $class = 'Icinga\\Application\\Logger\\Writer\\' . ucfirst(strtolower($config->log)) . 'Writer';
192        if (! class_exists($class)) {
193            throw new ConfigurationError(
194                'Cannot find log writer of type "%s"',
195                $config->log
196            );
197        }
198        return new $class($config);
199    }
200
201    /**
202     * Log a message
203     *
204     * @param   int     $level      The logging level
205     * @param   string  $message    The log message
206     */
207    public function log($level, $message)
208    {
209        if ($this->writer !== null && $this->level <= $level) {
210            foreach ($this->configErrors as $error_message) {
211                $this->writer->log(static::ERROR, $error_message);
212            }
213
214            $this->writer->log($level, $message);
215        }
216    }
217
218    /**
219     * Return a string representation of the passed arguments
220     *
221     * This method provides three different processing techniques:
222     *  - If the only passed argument is a string it is returned unchanged
223     *  - If the only passed argument is an exception it is formatted as follows:
224     *    <name> in <file>:<line> with message: <message>[ <- <name> ...]
225     *  - If multiple arguments are passed the first is interpreted as format-string
226     *    that gets substituted with the remaining ones which can be of any type
227     *
228     * @param   array   $arguments      The arguments to format
229     *
230     * @return  string                  The formatted result
231     */
232    protected static function formatMessage(array $arguments)
233    {
234        if (count($arguments) === 1) {
235            $message = $arguments[0];
236
237            if ($message instanceof Exception) {
238                $messages = array();
239                $error = $message;
240                do {
241                    $messages[] = IcingaException::describe($error);
242                } while ($error = $error->getPrevious());
243                $message = implode(' <- ', $messages);
244            }
245
246            return $message;
247        }
248
249        return vsprintf(
250            array_shift($arguments),
251            array_map(
252                function ($a) {
253                    return is_string($a) ? $a : ($a instanceof Exception
254                        ? IcingaException::describe($a)
255                        : Json::encode($a));
256                },
257                $arguments
258            )
259        );
260    }
261
262    /**
263     * Log a message with severity ERROR
264     *
265     * @param   mixed   $arg,...    A string, exception or format-string + substitutions
266     */
267    public static function error()
268    {
269        if (static::$instance !== null && func_num_args() > 0) {
270            static::$instance->log(static::ERROR, static::formatMessage(func_get_args()));
271        }
272    }
273
274    /**
275     * Log a message with severity WARNING
276     *
277     * @param   mixed   $arg,...    A string, exception or format-string + substitutions
278     */
279    public static function warning()
280    {
281        if (static::$instance !== null && func_num_args() > 0) {
282            static::$instance->log(static::WARNING, static::formatMessage(func_get_args()));
283        }
284    }
285
286    /**
287     * Log a message with severity INFO
288     *
289     * @param   mixed   $arg,...    A string, exception or format-string + substitutions
290     */
291    public static function info()
292    {
293        if (static::$instance !== null && func_num_args() > 0) {
294            static::$instance->log(static::INFO, static::formatMessage(func_get_args()));
295        }
296    }
297
298    /**
299     * Log a message with severity DEBUG
300     *
301     * @param   mixed   $arg,...    A string, exception or format-string + substitutions
302     */
303    public static function debug()
304    {
305        if (static::$instance !== null && func_num_args() > 0) {
306            static::$instance->log(static::DEBUG, static::formatMessage(func_get_args()));
307        }
308    }
309
310    /**
311     * Get the log writer to use
312     *
313     * @return \Icinga\Application\Logger\LogWriter
314     */
315    public function getWriter()
316    {
317        return $this->writer;
318    }
319
320    /**
321     * Is the logger writing to Syslog?
322     *
323     * @return bool
324     */
325    public static function writesToSyslog()
326    {
327        return static::$instance && static::$instance->getWriter() instanceof SyslogWriter;
328    }
329
330    /**
331     * Is the logger writing to a file?
332     *
333     * @return bool
334     */
335    public static function writesToFile()
336    {
337        return static::$instance && static::$instance->getWriter() instanceof FileWriter;
338    }
339
340    /**
341     * Get this' instance
342     *
343     * @return static
344     */
345    public static function getInstance()
346    {
347        return static::$instance;
348    }
349}
350