verbose = $verbose; if (is_dir($lpath) || strpos($lpath, '.') === false) { $path = $path . '/' . date('Y-m-d') . '.log'; } else { $path = $lpath; } $this->path = $path; } /** * Create logging file. * * @return void * * @since 1.0.0 */ private function createFile() /* : void */ { if (!$this->created && !file_exists($this->path)) { File::create($this->path); $this->created = true; } } /** * Returns instance. * * @param string $path Logging path * @param bool $verbose Verbose logging * * @return FileLogger * * @since 1.0.0 */ public static function getInstance(string $path = '', bool $verbose = false) : FileLogger { if (self::$instance === null) { self::$instance = new self($path, $verbose); } return self::$instance; } /** * Object destructor. * * Closes the logging file * * @since 1.0.0 * @codeCoverageIgnore */ public function __destruct() { if (is_resource($this->fp)) { fclose($this->fp); } } /** * Protect instance from getting copied from outside. * * @return void * * @since 1.0.0 * @codeCoverageIgnore */ private function __clone() { } /** * Starts the time measurement. * * @param string $id the ID by which this time measurement gets identified * * @return bool * * @since 1.0.0 */ public static function startTimeLog($id = '') : bool { if (isset(self::$timings[$id])) { return false; } $mtime = explode(' ', microtime()); $mtime = $mtime[1] + $mtime[0]; self::$timings[$id] = ['start' => $mtime]; return true; } /** * Ends the time measurement. * * @param string $id the ID by which this time measurement gets identified * * @return float The time measurement in ms * * @since 1.0.0 */ public static function endTimeLog($id = '') : float { $mtime = explode(' ', microtime()); $mtime = $mtime[1] + $mtime[0]; self::$timings[$id]['end'] = $mtime; self::$timings[$id]['time'] = $mtime - self::$timings[$id]['start']; return self::$timings[$id]['time']; } /** * Interpolate context * * @param string $message Log schema * @param array $context Context to log * @param string $level Log level * * @return string * * @since 1.0.0 */ private function interpolate(string $message, array $context = [], string $level = LogLevel::DEBUG) : string { $replace = []; foreach ($context as $key => $val) { $replace['{' . $key . '}'] = $val; } $backtrace = debug_backtrace(); // Removing sensitive config data from logging foreach ($backtrace as $key => $value) { if (isset($value['args'])) { unset($backtrace[$key]['args']); } } $backtrace = json_encode($backtrace); $replace['{backtrace}'] = $backtrace; $replace['{datetime}'] = sprintf('%--19s', (new \DateTime('NOW'))->format('Y-m-d H:i:s')); $replace['{level}'] = sprintf('%--12s', $level); $replace['{path}'] = $_SERVER['REQUEST_URI'] ?? 'REQUEST_URI'; $replace['{ip}'] = sprintf('%--15s', $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'); $replace['{version}'] = sprintf('%--15s', PHP_VERSION); $replace['{os}'] = sprintf('%--15s', PHP_OS); $replace['{line}'] = sprintf('%--15s', $context['line'] ?? '?'); return strtr($message, $replace); } /** * Write to file. * * @param string $message Log message * * @return void * * @since 1.0.0 */ private function write(string $message) /* : void */ { $this->createFile(); if (!is_writable($this->path)) { return; } $this->fp = fopen($this->path, 'a'); if (flock($this->fp, LOCK_EX) && $this->fp !== false) { fwrite($this->fp, $message . "\n"); fflush($this->fp); flock($this->fp, LOCK_UN); fclose($this->fp); $this->fp = false; } if ($this->verbose) { echo $message, "\n"; } } /** * {@inheritdoc} */ public function emergency(string $message, array $context = []) /* : void */ { $message = $this->interpolate($message, $context, LogLevel::EMERGENCY); $this->write($message); } /** * {@inheritdoc} */ public function alert(string $message, array $context = []) /* : void */ { $message = $this->interpolate($message, $context, LogLevel::ALERT); $this->write($message); } /** * {@inheritdoc} */ public function critical(string $message, array $context = []) /* : void */ { $message = $this->interpolate($message, $context, LogLevel::CRITICAL); $this->write($message); } /** * {@inheritdoc} */ public function error(string $message, array $context = []) /* : void */ { $message = $this->interpolate($message, $context, LogLevel::ERROR); $this->write($message); } /** * {@inheritdoc} */ public function warning(string $message, array $context = []) /* : void */ { $message = $this->interpolate($message, $context, LogLevel::WARNING); $this->write($message); } /** * {@inheritdoc} */ public function notice(string $message, array $context = []) /* : void */ { $message = $this->interpolate($message, $context, LogLevel::NOTICE); $this->write($message); } /** * {@inheritdoc} */ public function info(string $message, array $context = []) /* : void */ { $message = $this->interpolate($message, $context, LogLevel::INFO); $this->write($message); } /** * {@inheritdoc} */ public function debug(string $message, array $context = []) /* : void */ { $message = $this->interpolate($message, $context, LogLevel::DEBUG); $this->write($message); } /** * {@inheritdoc} */ public function log(string $level, string $message, array $context = []) /* : void */ { if (!LogLevel::isValidValue($level)) { throw new InvalidEnumValue($level); } $message = $this->interpolate($message, $context, $level); $this->write($message); } /** * Analyse logging file. * * @return array * * @since 1.0.0 */ public function countLogs() { $levels = []; if (!file_exists($this->path)) { return $levels; } $this->fp = fopen($this->path, 'r'); fseek($this->fp, 0); while (($line = fgetcsv($this->fp, 0, ';')) !== false) { $line[1] = trim($line[1]); if (!isset($levels[$line[1]])) { $levels[$line[1]] = 0; } $levels[$line[1]]++; } fseek($this->fp, 0, SEEK_END); fclose($this->fp); return $levels; } /** * Find cricitcal connections. * * @param int $limit Amout of perpetrators * * @return array * * @since 1.0.0 */ public function getHighestPerpetrator(int $limit = 10) : array { $connection = []; if (!file_exists($this->path)) { return $connection; } $this->fp = fopen($this->path, 'r'); fseek($this->fp, 0); while (($line = fgetcsv($this->fp, 0, ';')) !== false) { $line[2] = trim($line[2]); if (!isset($connection[$line[2]])) { $connection[$line[2]] = 0; } $connection[$line[2]]++; } fseek($this->fp, 0, SEEK_END); fclose($this->fp); asort($connection); return array_slice($connection, 0, $limit); } /** * Get logging messages from file. * * @param int $limit Amout of perpetrators * @param int $offset Offset * * @return array * * @since 1.0.0 */ public function get(int $limit = 25, int $offset = 0) : array { $logs = []; $id = 0; if (!file_exists($this->path)) { return $logs; } $this->fp = fopen($this->path, 'r'); fseek($this->fp, 0); while (($line = fgetcsv($this->fp, 0, ';')) !== false) { $id++; if ($offset > 0) { $offset--; continue; } if ($limit <= 0) { reset($logs); unset($logs[key($logs)]); } foreach ($line as &$value) { $value = trim($value); } $logs[$id] = $line; $limit--; ksort($logs); } fseek($this->fp, 0, SEEK_END); fclose($this->fp); return $logs; } /** * Get single logging message from file. * * @param int $id Id/Line number of the logging message * * @return array * * @since 1.0.0 */ public function getByLine(int $id = 1) : array { $log = []; $current = 0; if (!file_exists($this->path)) { return $log; } $this->fp = fopen($this->path, 'r'); fseek($this->fp, 0); while (($line = fgetcsv($this->fp, 0, ';')) !== false && $current <= $id) { $current++; if ($current < $id) { continue; } $log['datetime'] = trim($line[0] ?? ''); $log['level'] = trim($line[1] ?? ''); $log['ip'] = trim($line[2] ?? ''); $log['line'] = trim($line[3] ?? ''); $log['version'] = trim($line[4] ?? ''); $log['os'] = trim($line[5] ?? ''); $log['path'] = trim($line[6] ?? ''); $log['message'] = trim($line[7] ?? ''); $log['file'] = trim($line[8] ?? ''); $log['backtrace'] = trim($line[9] ?? ''); break; } fseek($this->fp, 0, SEEK_END); fclose($this->fp); return $log; } /** * Create console log. * * @param string $message Log message * @param bool $verbose Is verbose * @param array $context Context * * @return void * * @since 1.0.0 */ public function console(string $message, bool $verbose = true, array $context = []) /* : void */ { if (empty($context)) { $message = date('[Y-m-d H:i:s] ') . $message . "\r\n"; } if ($verbose) { echo $message; } else { $this->info($message, $context); } } }