xref: /dokuwiki/inc/Logger.php (revision f577a2efaaaefaee955f65130d5d7f54483f9be1)
10ecde6ceSAndreas Gohr<?php
20ecde6ceSAndreas Gohr
30ecde6ceSAndreas Gohrnamespace dokuwiki;
40ecde6ceSAndreas Gohr
5555e8b00SAndreas Gohruse dokuwiki\Extension\Event;
6555e8b00SAndreas Gohr
7555e8b00SAndreas Gohr/**
8555e8b00SAndreas Gohr * Log messages to a daily log file
9555e8b00SAndreas Gohr */
100ecde6ceSAndreas Gohrclass Logger
110ecde6ceSAndreas Gohr{
1274981a4eSAndreas Gohr    public const LOG_ERROR = 'error';
1374981a4eSAndreas Gohr    public const LOG_DEPRECATED = 'deprecated';
1474981a4eSAndreas Gohr    public const LOG_DEBUG = 'debug';
150ecde6ceSAndreas Gohr
160ecde6ceSAndreas Gohr    /** @var Logger[] */
17fe15e2c0SAndreas Gohr    protected static $instances;
180ecde6ceSAndreas Gohr
190ecde6ceSAndreas Gohr    /** @var string what kind of log is this */
200ecde6ceSAndreas Gohr    protected $facility;
210ecde6ceSAndreas Gohr
22cad4fbf6SAndreas Gohr    protected $isLogging = true;
23cad4fbf6SAndreas Gohr
24*f577a2efSAndreas Gohr    /** @var string[] a list of expected log messages, only used during unit testing */
25*f577a2efSAndreas Gohr    protected $expected = [];
26*f577a2efSAndreas Gohr
270ecde6ceSAndreas Gohr    /**
280ecde6ceSAndreas Gohr     * Logger constructor.
290ecde6ceSAndreas Gohr     *
300ecde6ceSAndreas Gohr     * @param string $facility The type of log
310ecde6ceSAndreas Gohr     */
320ecde6ceSAndreas Gohr    protected function __construct($facility)
330ecde6ceSAndreas Gohr    {
34cad4fbf6SAndreas Gohr        global $conf;
350ecde6ceSAndreas Gohr        $this->facility = $facility;
36cad4fbf6SAndreas Gohr
37cad4fbf6SAndreas Gohr        // Should logging be disabled for this facility?
38cad4fbf6SAndreas Gohr        $dontlog = explode(',', $conf['dontlog']);
39cad4fbf6SAndreas Gohr        $dontlog = array_map('trim', $dontlog);
40cad4fbf6SAndreas Gohr        if (in_array($facility, $dontlog)) $this->isLogging = false;
410ecde6ceSAndreas Gohr    }
420ecde6ceSAndreas Gohr
430ecde6ceSAndreas Gohr    /**
440ecde6ceSAndreas Gohr     * Return a Logger instance for the given facility
450ecde6ceSAndreas Gohr     *
460ecde6ceSAndreas Gohr     * @param string $facility The type of log
470ecde6ceSAndreas Gohr     * @return Logger
480ecde6ceSAndreas Gohr     */
49fe15e2c0SAndreas Gohr    public static function getInstance($facility = self::LOG_ERROR)
500ecde6ceSAndreas Gohr    {
51a8f9f939SDamien Regad        if (empty(self::$instances[$facility])) {
520ecde6ceSAndreas Gohr            self::$instances[$facility] = new Logger($facility);
530ecde6ceSAndreas Gohr        }
540ecde6ceSAndreas Gohr        return self::$instances[$facility];
550ecde6ceSAndreas Gohr    }
560ecde6ceSAndreas Gohr
570ecde6ceSAndreas Gohr    /**
58c2050393SAndreas Gohr     * Convenience method to directly log to the error log
59c2050393SAndreas Gohr     *
60c2050393SAndreas Gohr     * @param string $message The log message
61c2050393SAndreas Gohr     * @param mixed $details Any details that should be added to the log entry
62c2050393SAndreas Gohr     * @param string $file A source filename if this is related to a source position
63c2050393SAndreas Gohr     * @param int $line A line number for the above file
64cad4fbf6SAndreas Gohr     * @return bool has a log been written?
65c2050393SAndreas Gohr     */
66fe15e2c0SAndreas Gohr    public static function error($message, $details = null, $file = '', $line = 0)
67c2050393SAndreas Gohr    {
68c2050393SAndreas Gohr        return self::getInstance(self::LOG_ERROR)->log(
69dccd6b2bSAndreas Gohr            $message,
70dccd6b2bSAndreas Gohr            $details,
71dccd6b2bSAndreas Gohr            $file,
72dccd6b2bSAndreas Gohr            $line
73c2050393SAndreas Gohr        );
74c2050393SAndreas Gohr    }
75c2050393SAndreas Gohr
76c2050393SAndreas Gohr    /**
77c2050393SAndreas Gohr     * Convenience method to directly log to the debug log
78c2050393SAndreas Gohr     *
79c2050393SAndreas Gohr     * @param string $message The log message
80c2050393SAndreas Gohr     * @param mixed $details Any details that should be added to the log entry
81c2050393SAndreas Gohr     * @param string $file A source filename if this is related to a source position
82c2050393SAndreas Gohr     * @param int $line A line number for the above file
83cad4fbf6SAndreas Gohr     * @return bool has a log been written?
84c2050393SAndreas Gohr     */
85fe15e2c0SAndreas Gohr    public static function debug($message, $details = null, $file = '', $line = 0)
86c2050393SAndreas Gohr    {
87c2050393SAndreas Gohr        return self::getInstance(self::LOG_DEBUG)->log(
88dccd6b2bSAndreas Gohr            $message,
89dccd6b2bSAndreas Gohr            $details,
90dccd6b2bSAndreas Gohr            $file,
91dccd6b2bSAndreas Gohr            $line
92c2050393SAndreas Gohr        );
93c2050393SAndreas Gohr    }
94c2050393SAndreas Gohr
95c2050393SAndreas Gohr    /**
96c2050393SAndreas Gohr     * Convenience method to directly log to the deprecation log
97c2050393SAndreas Gohr     *
98c2050393SAndreas Gohr     * @param string $message The log message
99c2050393SAndreas Gohr     * @param mixed $details Any details that should be added to the log entry
100c2050393SAndreas Gohr     * @param string $file A source filename if this is related to a source position
101c2050393SAndreas Gohr     * @param int $line A line number for the above file
102cad4fbf6SAndreas Gohr     * @return bool has a log been written?
103c2050393SAndreas Gohr     */
104fe15e2c0SAndreas Gohr    public static function deprecated($message, $details = null, $file = '', $line = 0)
105c2050393SAndreas Gohr    {
106c2050393SAndreas Gohr        return self::getInstance(self::LOG_DEPRECATED)->log(
107dccd6b2bSAndreas Gohr            $message,
108dccd6b2bSAndreas Gohr            $details,
109dccd6b2bSAndreas Gohr            $file,
110dccd6b2bSAndreas Gohr            $line
111c2050393SAndreas Gohr        );
112c2050393SAndreas Gohr    }
113c2050393SAndreas Gohr
114c2050393SAndreas Gohr    /**
1150ecde6ceSAndreas Gohr     * Log a message to the facility log
1160ecde6ceSAndreas Gohr     *
1170ecde6ceSAndreas Gohr     * @param string $message The log message
1180ecde6ceSAndreas Gohr     * @param mixed $details Any details that should be added to the log entry
1190ecde6ceSAndreas Gohr     * @param string $file A source filename if this is related to a source position
1200ecde6ceSAndreas Gohr     * @param int $line A line number for the above file
121555e8b00SAndreas Gohr     * @triggers LOGGER_DATA_FORMAT can be used to change the logged data or intercept it
122cad4fbf6SAndreas Gohr     * @return bool has a log been written?
1230ecde6ceSAndreas Gohr     */
1240ecde6ceSAndreas Gohr    public function log($message, $details = null, $file = '', $line = 0)
1250ecde6ceSAndreas Gohr    {
1264b647920SAndreas Gohr        global $EVENT_HANDLER;
127cad4fbf6SAndreas Gohr        if (!$this->isLogging) return false;
128cad4fbf6SAndreas Gohr
129555e8b00SAndreas Gohr        $datetime = time();
130555e8b00SAndreas Gohr        $data = [
131555e8b00SAndreas Gohr            'facility' => $this->facility,
132555e8b00SAndreas Gohr            'datetime' => $datetime,
133555e8b00SAndreas Gohr            'message' => $message,
134555e8b00SAndreas Gohr            'details' => $details,
135555e8b00SAndreas Gohr            'file' => $file,
136555e8b00SAndreas Gohr            'line' => $line,
137555e8b00SAndreas Gohr            'loglines' => [],
138555e8b00SAndreas Gohr            'logfile' => $this->getLogfile($datetime),
139555e8b00SAndreas Gohr        ];
140555e8b00SAndreas Gohr
1414b647920SAndreas Gohr        if ($EVENT_HANDLER !== null) {
1424b647920SAndreas Gohr            $event = new Event('LOGGER_DATA_FORMAT', $data);
143555e8b00SAndreas Gohr            if ($event->advise_before()) {
144555e8b00SAndreas Gohr                $data['loglines'] = $this->formatLogLines($data);
145555e8b00SAndreas Gohr            }
146555e8b00SAndreas Gohr            $event->advise_after();
1474b647920SAndreas Gohr        } else {
1484b647920SAndreas Gohr            // The event system is not yet available, to ensure the log isn't lost even on
1494b647920SAndreas Gohr            // fatal errors, the default action is executed
1504b647920SAndreas Gohr            $data['loglines'] = $this->formatLogLines($data);
1514b647920SAndreas Gohr        }
152555e8b00SAndreas Gohr
153555e8b00SAndreas Gohr        // only log when any data available
154555e8b00SAndreas Gohr        if (count($data['loglines'])) {
155555e8b00SAndreas Gohr            return $this->writeLogLines($data['loglines'], $data['logfile']);
156555e8b00SAndreas Gohr        } else {
157555e8b00SAndreas Gohr            return false;
158555e8b00SAndreas Gohr        }
159555e8b00SAndreas Gohr    }
160555e8b00SAndreas Gohr
161555e8b00SAndreas Gohr    /**
16225edeecaSAndreas Gohr     * Is this logging instace actually logging?
16325edeecaSAndreas Gohr     *
16425edeecaSAndreas Gohr     * @return bool
16525edeecaSAndreas Gohr     */
16625edeecaSAndreas Gohr    public function isLogging()
16725edeecaSAndreas Gohr    {
16825edeecaSAndreas Gohr        return $this->isLogging;
16925edeecaSAndreas Gohr    }
17025edeecaSAndreas Gohr
17125edeecaSAndreas Gohr    /**
172*f577a2efSAndreas Gohr     * Tests may register log expectations
173*f577a2efSAndreas Gohr     *
174*f577a2efSAndreas Gohr     * @param string $log
175*f577a2efSAndreas Gohr     * @return void
176*f577a2efSAndreas Gohr     */
177*f577a2efSAndreas Gohr    public function expect($log)
178*f577a2efSAndreas Gohr    {
179*f577a2efSAndreas Gohr        $this->expected[] = $log;
180*f577a2efSAndreas Gohr    }
181*f577a2efSAndreas Gohr
182*f577a2efSAndreas Gohr    /**
183555e8b00SAndreas Gohr     * Formats the given data as loglines
184555e8b00SAndreas Gohr     *
185555e8b00SAndreas Gohr     * @param array $data Event data from LOGGER_DATA_FORMAT
186555e8b00SAndreas Gohr     * @return string[] the lines to log
187555e8b00SAndreas Gohr     */
188555e8b00SAndreas Gohr    protected function formatLogLines($data)
189555e8b00SAndreas Gohr    {
190555e8b00SAndreas Gohr        extract($data);
191555e8b00SAndreas Gohr
1920ecde6ceSAndreas Gohr        // details are logged indented
19370cc2cbfSAndreas Gohr        if ($details) {
19470cc2cbfSAndreas Gohr            if (!is_string($details)) {
1950ecde6ceSAndreas Gohr                $details = json_encode($details, JSON_PRETTY_PRINT);
19670cc2cbfSAndreas Gohr            }
1970ecde6ceSAndreas Gohr            $details = explode("\n", $details);
19824870174SAndreas Gohr            $loglines = array_map(static fn($line) => '  ' . $line, $details);
1990ecde6ceSAndreas Gohr        } elseif ($details) {
2000ecde6ceSAndreas Gohr            $loglines = [$details];
2010ecde6ceSAndreas Gohr        } else {
2020ecde6ceSAndreas Gohr            $loglines = [];
2030ecde6ceSAndreas Gohr        }
2040ecde6ceSAndreas Gohr
20570cc2cbfSAndreas Gohr        // datetime, fileline, message
206d4059ee7Shauk92        $logline = date('Y-m-d H:i:s', $datetime) . "\t";
2070ecde6ceSAndreas Gohr        if ($file) {
20870cc2cbfSAndreas Gohr            $logline .= $file;
2090ecde6ceSAndreas Gohr            if ($line) $logline .= "($line)";
2100ecde6ceSAndreas Gohr        }
21170cc2cbfSAndreas Gohr        $logline .= "\t" . $message;
2120ecde6ceSAndreas Gohr        array_unshift($loglines, $logline);
213555e8b00SAndreas Gohr
214555e8b00SAndreas Gohr        return $loglines;
2150ecde6ceSAndreas Gohr    }
2160ecde6ceSAndreas Gohr
2170ecde6ceSAndreas Gohr    /**
21870cc2cbfSAndreas Gohr     * Construct the log file for the given day
21970cc2cbfSAndreas Gohr     *
22070cc2cbfSAndreas Gohr     * @param false|string|int $date Date to access, false for today
22170cc2cbfSAndreas Gohr     * @return string
22270cc2cbfSAndreas Gohr     */
22370cc2cbfSAndreas Gohr    public function getLogfile($date = false)
22470cc2cbfSAndreas Gohr    {
22570cc2cbfSAndreas Gohr        global $conf;
22670cc2cbfSAndreas Gohr
227baa301e2SAndreas Gohr        if ($date !== null && !is_numeric($date)) {
228baa301e2SAndreas Gohr            $date = strtotime($date);
229baa301e2SAndreas Gohr        }
23070cc2cbfSAndreas Gohr        if (!$date) $date = time();
23170cc2cbfSAndreas Gohr
23270cc2cbfSAndreas Gohr        return $conf['logdir'] . '/' . $this->facility . '/' . date('Y-m-d', $date) . '.log';
23370cc2cbfSAndreas Gohr    }
23470cc2cbfSAndreas Gohr
23570cc2cbfSAndreas Gohr    /**
2360ecde6ceSAndreas Gohr     * Write the given lines to today's facility log
2370ecde6ceSAndreas Gohr     *
2380ecde6ceSAndreas Gohr     * @param string[] $lines the raw lines to append to the log
239555e8b00SAndreas Gohr     * @param string $logfile where to write to
2400ecde6ceSAndreas Gohr     * @return bool true if the log was written
2410ecde6ceSAndreas Gohr     */
242555e8b00SAndreas Gohr    protected function writeLogLines($lines, $logfile)
2430ecde6ceSAndreas Gohr    {
2446c6732d6SAndreas Gohr        if (defined('DOKU_UNITTEST')) {
245*f577a2efSAndreas Gohr            // our tests may expect certain log messages
246*f577a2efSAndreas Gohr            if($this->expected) {
247*f577a2efSAndreas Gohr                $expected = array_shift($this->expected);
248*f577a2efSAndreas Gohr                if(!str_contains($lines[0], $expected)) {
249*f577a2efSAndreas Gohr                    throw new \RuntimeException(
250*f577a2efSAndreas Gohr                        "Log expectation failed:\n".
251*f577a2efSAndreas Gohr                        "Expected: $expected\n".
252*f577a2efSAndreas Gohr                        "Actual:   {$lines[0]}"
253*f577a2efSAndreas Gohr                    );
254*f577a2efSAndreas Gohr                }
255*f577a2efSAndreas Gohr            } else {
256521819ffSAndreas Gohr                $stderr = fopen('php://stderr', 'w');
257521819ffSAndreas Gohr                fwrite($stderr, "\n[" . $this->facility . '] ' . implode("\n", $lines) . "\n");
258521819ffSAndreas Gohr                fclose($stderr);
2596c6732d6SAndreas Gohr            }
260*f577a2efSAndreas Gohr        }
261*f577a2efSAndreas Gohr
26224870174SAndreas Gohr        return io_saveFile($logfile, implode("\n", $lines) . "\n", true);
2630ecde6ceSAndreas Gohr    }
2640ecde6ceSAndreas Gohr}
265