xref: /dokuwiki/inc/Logger.php (revision 54cc7aa41e0f453bd6887b0e79242a139d84a47a)
1<?php
2
3namespace dokuwiki;
4
5use dokuwiki\Extension\Event;
6
7/**
8 * Log messages to a daily log file
9 */
10class Logger
11{
12    const LOG_ERROR = 'error';
13    const LOG_DEPRECATED = 'deprecated';
14    const LOG_DEBUG = 'debug';
15
16    /** @var Logger[] */
17    static protected $instances;
18
19    /** @var string what kind of log is this */
20    protected $facility;
21
22    protected $isLogging = true;
23
24    /**
25     * Logger constructor.
26     *
27     * @param string $facility The type of log
28     */
29    protected function __construct($facility)
30    {
31        global $conf;
32        $this->facility = $facility;
33
34        // Should logging be disabled for this facility?
35        $dontlog = explode(',', $conf['dontlog']);
36        $dontlog = array_map('trim', $dontlog);
37        if (in_array($facility, $dontlog)) $this->isLogging = false;
38    }
39
40    /**
41     * Return a Logger instance for the given facility
42     *
43     * @param string $facility The type of log
44     * @return Logger
45     */
46    static public function getInstance($facility = self::LOG_ERROR)
47    {
48        if (empty(self::$instances[$facility])) {
49            self::$instances[$facility] = new Logger($facility);
50        }
51        return self::$instances[$facility];
52    }
53
54    /**
55     * Convenience method to directly log to the error log
56     *
57     * @param string $message The log message
58     * @param mixed $details Any details that should be added to the log entry
59     * @param string $file A source filename if this is related to a source position
60     * @param int $line A line number for the above file
61     * @return bool has a log been written?
62     */
63    static public function error($message, $details = null, $file = '', $line = 0)
64    {
65        return self::getInstance(self::LOG_ERROR)->log(
66            $message, $details, $file, $line
67        );
68    }
69
70    /**
71     * Convenience method to directly log to the debug log
72     *
73     * @param string $message The log message
74     * @param mixed $details Any details that should be added to the log entry
75     * @param string $file A source filename if this is related to a source position
76     * @param int $line A line number for the above file
77     * @return bool has a log been written?
78     */
79    static public function debug($message, $details = null, $file = '', $line = 0)
80    {
81        return self::getInstance(self::LOG_DEBUG)->log(
82            $message, $details, $file, $line
83        );
84    }
85
86    /**
87     * Convenience method to directly log to the deprecation log
88     *
89     * @param string $message The log message
90     * @param mixed $details Any details that should be added to the log entry
91     * @param string $file A source filename if this is related to a source position
92     * @param int $line A line number for the above file
93     * @return bool has a log been written?
94     */
95    static public function deprecated($message, $details = null, $file = '', $line = 0)
96    {
97        return self::getInstance(self::LOG_DEPRECATED)->log(
98            $message, $details, $file, $line
99        );
100    }
101
102    /**
103     * Log a message to the facility log
104     *
105     * @param string $message The log message
106     * @param mixed $details Any details that should be added to the log entry
107     * @param string $file A source filename if this is related to a source position
108     * @param int $line A line number for the above file
109     * @triggers LOGGER_DATA_FORMAT can be used to change the logged data or intercept it
110     * @return bool has a log been written?
111     */
112    public function log($message, $details = null, $file = '', $line = 0)
113    {
114        global $EVENT_HANDLER;
115        if (!$this->isLogging) return false;
116
117        $datetime = time();
118        $data = [
119            'facility' => $this->facility,
120            'datetime' => $datetime,
121            'message' => $message,
122            'details' => $details,
123            'file' => $file,
124            'line' => $line,
125            'loglines' => [],
126            'logfile' => $this->getLogfile($datetime),
127        ];
128
129        if ($EVENT_HANDLER !== null) {
130            $event = new Event('LOGGER_DATA_FORMAT', $data);
131            if ($event->advise_before()) {
132                $data['loglines'] = $this->formatLogLines($data);
133            }
134            $event->advise_after();
135        } else {
136            // The event system is not yet available, to ensure the log isn't lost even on
137            // fatal errors, the default action is executed
138            $data['loglines'] = $this->formatLogLines($data);
139        }
140
141        // only log when any data available
142        if (count($data['loglines'])) {
143            return $this->writeLogLines($data['loglines'], $data['logfile']);
144        } else {
145            return false;
146        }
147    }
148
149    /**
150     * Is this logging instace actually logging?
151     *
152     * @return bool
153     */
154    public function isLogging()
155    {
156        return $this->isLogging;
157    }
158
159    /**
160     * Formats the given data as loglines
161     *
162     * @param array $data Event data from LOGGER_DATA_FORMAT
163     * @return string[] the lines to log
164     */
165    protected function formatLogLines($data)
166    {
167        extract($data);
168
169        // details are logged indented
170        if ($details) {
171            if (!is_string($details)) {
172                $details = json_encode($details, JSON_PRETTY_PRINT);
173            }
174            $details = explode("\n", $details);
175            $loglines = array_map(static fn($line) => '  ' . $line, $details);
176        } elseif ($details) {
177            $loglines = [$details];
178        } else {
179            $loglines = [];
180        }
181
182        // datetime, fileline, message
183        $logline = gmdate('Y-m-d H:i:s', $datetime) . "\t";
184        if ($file) {
185            $logline .= $file;
186            if ($line) $logline .= "($line)";
187        }
188        $logline .= "\t" . $message;
189        array_unshift($loglines, $logline);
190
191        return $loglines;
192    }
193
194    /**
195     * Construct the log file for the given day
196     *
197     * @param false|string|int $date Date to access, false for today
198     * @return string
199     */
200    public function getLogfile($date = false)
201    {
202        global $conf;
203
204        if ($date !== null && !is_numeric($date)) {
205            $date = strtotime($date);
206        }
207        if (!$date) $date = time();
208
209        return $conf['logdir'] . '/' . $this->facility . '/' . date('Y-m-d', $date) . '.log';
210    }
211
212    /**
213     * Write the given lines to today's facility log
214     *
215     * @param string[] $lines the raw lines to append to the log
216     * @param string $logfile where to write to
217     * @return bool true if the log was written
218     */
219    protected function writeLogLines($lines, $logfile)
220    {
221        if (defined('DOKU_UNITTEST')) {
222            fwrite(STDERR, "\n[" . $this->facility . '] ' . implode("\n", $lines) . "\n");
223        }
224        return io_saveFile($logfile, implode("\n", $lines) . "\n", true);
225    }
226}
227