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