From: Philipp Date: Sat, 23 Oct 2021 10:22:27 +0000 (+0200) Subject: Restructure Logger to new paradigm X-Git-Url: https://git.mxchange.org/?a=commitdiff_plain;h=184f6cc255ff40e4db428bc256127403b03bd087;p=friendica.git Restructure Logger to new paradigm --- diff --git a/src/Core/Logger.php b/src/Core/Logger.php index 6dde142cc9..4e2575d1b9 100644 --- a/src/Core/Logger.php +++ b/src/Core/Logger.php @@ -22,7 +22,7 @@ namespace Friendica\Core; use Friendica\DI; -use Friendica\Util\Logger\WorkerLogger; +use Friendica\Core\Logger\Type\WorkerLogger; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; diff --git a/src/Core/Logger/Exception/LoggerArgumentException.php b/src/Core/Logger/Exception/LoggerArgumentException.php new file mode 100644 index 0000000000..1b5f653f43 --- /dev/null +++ b/src/Core/Logger/Exception/LoggerArgumentException.php @@ -0,0 +1,13 @@ +. + * + */ + +namespace Friendica\Core\Logger\Factory; + +use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Core\Logger\Exception\LoggerException; +use Friendica\Core; +use Friendica\Database\Database; +use Friendica\Util\FileSystem; +use Friendica\Util\Introspection; +use Friendica\Core\Logger\Type\Monolog\DevelopHandler; +use Friendica\Core\Logger\Type\Monolog\IntrospectionProcessor; +use Friendica\Core\Logger\Type\ProfilerLogger; +use Friendica\Core\Logger\Type\StreamLogger; +use Friendica\Core\Logger\Type\SyslogLogger; +use Friendica\Core\Logger\Type\VoidLogger; +use Friendica\Util\Profiler; +use Monolog; +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; + +/** + * A logger factory + */ +class Logger +{ + const DEV_CHANNEL = 'dev'; + + /** + * A list of classes, which shouldn't get logged + * + * @var string[] + */ + private static $ignoreClassList = [ + Core\Logger::class, + Profiler::class, + 'Friendica\\Core\\Logger\\Type', + 'Friendica\\Core\\Logger\\Type\\Monolog', + ]; + + /** @var string The log-channel (app, worker, ...) */ + private $channel; + + public function __construct(string $channel) + { + $this->channel = $channel; + } + + /** + * Creates a new PSR-3 compliant logger instances + * + * @param Database $database The Friendica Database instance + * @param IManageConfigValues $config The config + * @param Profiler $profiler The profiler of the app + * @param FileSystem $fileSystem FileSystem utils + * + * @return LoggerInterface The PSR-3 compliant logger instance + */ + public function create(Database $database, IManageConfigValues $config, Profiler $profiler, FileSystem $fileSystem): LoggerInterface + { + if (empty($config->get('system', 'debugging', false))) { + $logger = new VoidLogger(); + $database->setLogger($logger); + return $logger; + } + + $introspection = new Introspection(self::$ignoreClassList); + $level = $config->get('system', 'loglevel'); + $loglevel = self::mapLegacyConfigDebugLevel((string)$level); + + switch ($config->get('system', 'logger_config', 'stream')) { + case 'monolog': + $loggerTimeZone = new \DateTimeZone('UTC'); + Monolog\Logger::setTimezone($loggerTimeZone); + + $logger = new Monolog\Logger($this->channel); + $logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor()); + $logger->pushProcessor(new Monolog\Processor\ProcessIdProcessor()); + $logger->pushProcessor(new Monolog\Processor\UidProcessor()); + $logger->pushProcessor(new IntrospectionProcessor($introspection, LogLevel::DEBUG)); + + $stream = $config->get('system', 'logfile'); + + // just add a stream in case it's either writable or not file + if (!is_file($stream) || is_writable($stream)) { + try { + static::addStreamHandler($logger, $stream, $loglevel); + } catch (\Throwable $e) { + // No Logger .. + /// @todo isn't it possible to give the admin any hint about this wrong configuration? + $logger = new VoidLogger(); + } + } + break; + + case 'syslog': + try { + $logger = new SyslogLogger($this->channel, $introspection, $loglevel); + } catch (\Throwable $e) { + // No logger ... + /// @todo isn't it possible to give the admin any hint about this wrong configuration? + $logger = new VoidLogger(); + } + break; + + case 'stream': + default: + $stream = $config->get('system', 'logfile'); + // just add a stream in case it's either writable or not file + if (!is_file($stream) || is_writable($stream)) { + try { + $logger = new StreamLogger($this->channel, $stream, $introspection, $fileSystem, $loglevel); + } catch (\Throwable $t) { + // No logger ... + /// @todo isn't it possible to give the admin any hint about this wrong configuration? + $logger = new VoidLogger(); + } + } else { + /// @todo isn't it possible to give the admin any hint about this wrong configuration? + $logger = new VoidLogger(); + } + break; + } + + $profiling = $config->get('system', 'profiling', false); + + // In case profiling is enabled, wrap the ProfilerLogger around the current logger + if (isset($profiling) && $profiling !== false) { + $logger = new ProfilerLogger($logger, $profiler); + } + + $database->setLogger($logger); + return $logger; + } + + /** + * Creates a new PSR-3 compliant develop logger + * + * If you want to debug only interactions from your IP or the IP of a remote server for federation debug, + * you'll use this logger instance for the duration of your work. + * + * It should never get filled during normal usage of Friendica + * + * @param IManageConfigValues $config The config + * @param Profiler $profiler The profiler of the app + * @param FileSystem $fileSystem FileSystem utils + * + * @return LoggerInterface The PSR-3 compliant logger instance + * @throws \Exception + */ + public static function createDev(IManageConfigValues $config, Profiler $profiler, FileSystem $fileSystem) + { + $debugging = $config->get('system', 'debugging'); + $stream = $config->get('system', 'dlogfile'); + $developerIp = $config->get('system', 'dlogip'); + + if ((!isset($developerIp) || !$debugging) && + (!is_file($stream) || is_writable($stream))) { + return new VoidLogger(); + } + + $loggerTimeZone = new \DateTimeZone('UTC'); + Monolog\Logger::setTimezone($loggerTimeZone); + + $introspection = new Introspection(self::$ignoreClassList); + + switch ($config->get('system', 'logger_config', 'stream')) { + + case 'monolog': + $loggerTimeZone = new \DateTimeZone('UTC'); + Monolog\Logger::setTimezone($loggerTimeZone); + + $logger = new Monolog\Logger(self::DEV_CHANNEL); + $logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor()); + $logger->pushProcessor(new Monolog\Processor\ProcessIdProcessor()); + $logger->pushProcessor(new Monolog\Processor\UidProcessor()); + $logger->pushProcessor(new IntrospectionProcessor($introspection, LogLevel::DEBUG)); + + $logger->pushHandler(new DevelopHandler($developerIp)); + + static::addStreamHandler($logger, $stream, LogLevel::DEBUG); + break; + + case 'syslog': + $logger = new SyslogLogger(self::DEV_CHANNEL, $introspection, LogLevel::DEBUG); + break; + + case 'stream': + default: + $logger = new StreamLogger(self::DEV_CHANNEL, $stream, $introspection, $fileSystem, LogLevel::DEBUG); + break; + } + + $profiling = $config->get('system', 'profiling', false); + + // In case profiling is enabled, wrap the ProfilerLogger around the current logger + if (isset($profiling) && $profiling !== false) { + $logger = new ProfilerLogger($logger, $profiler); + } + + return $logger; + } + + /** + * Mapping a legacy level to the PSR-3 compliant levels + * + * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md#5-psrlogloglevel + * + * @param string $level the level to be mapped + * + * @return string the PSR-3 compliant level + */ + private static function mapLegacyConfigDebugLevel(string $level): string + { + switch ($level) { + // legacy WARNING + case "0": + return LogLevel::ERROR; + // legacy INFO + case "1": + return LogLevel::WARNING; + // legacy TRACE + case "2": + return LogLevel::NOTICE; + // legacy DEBUG + case "3": + return LogLevel::INFO; + // legacy DATA + case "4": + // legacy ALL + case "5": + return LogLevel::DEBUG; + // default if nothing set + default: + return $level; + } + } + + /** + * Adding a handler to a given logger instance + * + * @param LoggerInterface $logger The logger instance + * @param mixed $stream The stream which handles the logger output + * @param string $level The level, for which this handler at least should handle logging + * + * @return void + * + * @throws LoggerException + */ + public static function addStreamHandler(LoggerInterface $logger, $stream, string $level = LogLevel::NOTICE) + { + if ($logger instanceof Monolog\Logger) { + $loglevel = Monolog\Logger::toMonologLevel($level); + + // fallback to notice if an invalid loglevel is set + if (!is_int($loglevel)) { + $loglevel = LogLevel::NOTICE; + } + + try { + $fileHandler = new Monolog\Handler\StreamHandler($stream, $loglevel); + + $formatter = new Monolog\Formatter\LineFormatter("%datetime% %channel% [%level_name%]: %message% %context% %extra%\n"); + $fileHandler->setFormatter($formatter); + + $logger->pushHandler($fileHandler); + } catch (\Exception $exception) { + throw new LoggerException('Cannot create Monolog Logger.', $exception); + } + } + } +} diff --git a/src/Core/Logger/Type/AbstractLogger.php b/src/Core/Logger/Type/AbstractLogger.php new file mode 100644 index 0000000000..0b6d9f38fd --- /dev/null +++ b/src/Core/Logger/Type/AbstractLogger.php @@ -0,0 +1,206 @@ +. + * + */ + +namespace Friendica\Core\Logger\Type; + +use Friendica\Core\Logger\Exception\LoggerException; +use Friendica\Util\Introspection; +use Friendica\Util\Strings; +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; + +/** + * This class contains all necessary dependencies and calls for Friendica + * Every new Logger should extend this class and define, how addEntry() works + * + * Additional information for each Logger, who extends this class: + * - Introspection + * - UID for each call + * - Channel of the current call (i.e. index, worker, daemon, ...) + */ +abstract class AbstractLogger implements LoggerInterface +{ + /** + * The output channel of this logger + * @var string + */ + protected $channel; + + /** + * The Introspection for the current call + * @var Introspection + */ + protected $introspection; + + /** + * The UID of the current call + * @var string + */ + protected $logUid; + + /** + * Adds a new entry to the log + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + */ + abstract protected function addEntry($level, string $message, array $context = []); + + /** + * @param string $channel The output channel + * @param Introspection $introspection The introspection of the current call + * + * @throws LoggerException + */ + public function __construct(string $channel, Introspection $introspection) + { + $this->channel = $channel; + $this->introspection = $introspection; + + try { + $this->logUid = Strings::getRandomHex(6); + } catch (\Exception $exception) { + throw new LoggerException('Cannot generate log Id', $exception); + } + } + + /** + * Simple interpolation of PSR-3 compliant replacements ( variables between '{' and '}' ) + * + * @see https://www.php-fig.org/psr/psr-3/#12-message + * + * @param string $message + * @param array $context + * + * @return string the interpolated message + */ + protected function psrInterpolate(string $message, array $context = []): string + { + $replace = []; + foreach ($context as $key => $value) { + // check that the value can be casted to string + if (!is_array($value) && (!is_object($value) || method_exists($value, '__toString'))) { + $replace['{' . $key . '}'] = $value; + } elseif (is_array($value)) { + $replace['{' . $key . '}'] = @json_encode($value); + } + } + + return strtr($message, $replace); + } + + /** + * JSON Encodes a complete array including objects with "__toString()" methods + * + * @param array $input an Input Array to encode + * + * @return false|string The json encoded output of the array + */ + protected function jsonEncodeArray(array $input) + { + $output = []; + + foreach ($input as $key => $value) { + if (is_object($value) && method_exists($value, '__toString')) { + $output[$key] = $value->__toString(); + } else { + $output[$key] = $value; + } + } + + return @json_encode($output); + } + + /** + * {@inheritdoc} + */ + public function emergency($message, array $context = []) + { + $this->addEntry(LogLevel::EMERGENCY, (string) $message, $context); + } + + /** + * {@inheritdoc} + */ + public function alert($message, array $context = []) + { + $this->addEntry(LogLevel::ALERT, (string) $message, $context); + } + + /** + * {@inheritdoc} + */ + public function critical($message, array $context = []) + { + $this->addEntry(LogLevel::CRITICAL, (string) $message, $context); + } + + /** + * {@inheritdoc} + */ + public function error($message, array $context = []) + { + $this->addEntry(LogLevel::ERROR, (string) $message, $context); + } + + /** + * {@inheritdoc} + */ + public function warning($message, array $context = []) + { + $this->addEntry(LogLevel::WARNING, (string) $message, $context); + } + + /** + * {@inheritdoc} + */ + public function notice($message, array $context = []) + { + $this->addEntry(LogLevel::NOTICE, (string) $message, $context); + } + + /** + * {@inheritdoc} + */ + public function info($message, array $context = []) + { + $this->addEntry(LogLevel::INFO, (string) $message, $context); + } + + /** + * {@inheritdoc} + */ + public function debug($message, array $context = []) + { + $this->addEntry(LogLevel::DEBUG, (string) $message, $context); + } + + /** + * {@inheritdoc} + */ + public function log($level, $message, array $context = []) + { + $this->addEntry($level, (string) $message, $context); + } +} diff --git a/src/Core/Logger/Type/Monolog/DevelopHandler.php b/src/Core/Logger/Type/Monolog/DevelopHandler.php new file mode 100644 index 0000000000..5d2a84401c --- /dev/null +++ b/src/Core/Logger/Type/Monolog/DevelopHandler.php @@ -0,0 +1,68 @@ +. + * + */ + +namespace Friendica\Core\Logger\Type\Monolog; + +use Monolog\Handler; +use Monolog\Logger; + +/** + * Simple handler for Friendica developers to use for deeper logging + * + * If you want to debug only interactions from your IP or the IP of a remote server for federation debug, + * you'll use Logger::develop() for the duration of your work, and you clean it up when you're done before submitting your PR. + */ +class DevelopHandler extends Handler\AbstractHandler +{ + /** + * @var string The IP of the developer who wants to debug + */ + private $developerIp; + + /** + * @param string $developerIp The IP of the developer who wants to debug + * @param int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct($developerIp, $level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + + $this->developerIp = $developerIp; + } + + /** + * {@inheritdoc} + */ + public function handle(array $record): bool + { + if (!$this->isHandling($record)) { + return false; + } + + /// Just in case the remote IP is the same as the developer IP log the output + if (!is_null($this->developerIp) && $_SERVER['REMOTE_ADDR'] != $this->developerIp) { + return false; + } + + return false === $this->bubble; + } +} diff --git a/src/Core/Logger/Type/Monolog/IntrospectionProcessor.php b/src/Core/Logger/Type/Monolog/IntrospectionProcessor.php new file mode 100644 index 0000000000..756331b22f --- /dev/null +++ b/src/Core/Logger/Type/Monolog/IntrospectionProcessor.php @@ -0,0 +1,62 @@ +. + * + */ + +namespace Friendica\Core\Logger\Type\Monolog; + +use Friendica\Util\Introspection; +use Monolog\Logger; +use Monolog\Processor\ProcessorInterface; + +/** + * Injects line/file//function where the log message came from + */ +class IntrospectionProcessor implements ProcessorInterface +{ + private $level; + + private $introspection; + + /** + * @param Introspection $introspection Holds the Introspection of the current call + * @param string|int $level The minimum logging level at which this Processor will be triggered + */ + public function __construct(Introspection $introspection, $level = Logger::DEBUG) + { + $this->level = Logger::toMonologLevel($level); + $introspection->addClasses(['Monolog\\']); + $this->introspection = $introspection; + } + + public function __invoke(array $record): array + { + // return if the level is not high enough + if ($record['level'] < $this->level) { + return $record; + } + // we should have the call source now + $record['extra'] = array_merge( + $record['extra'], + $this->introspection->getRecord() + ); + + return $record; + } +} diff --git a/src/Core/Logger/Type/ProfilerLogger.php b/src/Core/Logger/Type/ProfilerLogger.php new file mode 100644 index 0000000000..2333dd5403 --- /dev/null +++ b/src/Core/Logger/Type/ProfilerLogger.php @@ -0,0 +1,145 @@ +. + * + */ + +namespace Friendica\Core\Logger\Type; + +use Friendica\Util\Profiler; +use Psr\Log\LoggerInterface; + +/** + * This Logger adds additional profiling data in case profiling is enabled. + * It uses a predefined logger. + */ +class ProfilerLogger implements LoggerInterface +{ + /** + * The Logger of the current call + * @var LoggerInterface + */ + private $logger; + + /** + * The Profiler for the current call + * @var Profiler + */ + protected $profiler; + + /** + * ProfilerLogger constructor. + * @param LoggerInterface $logger The Logger of the current call + * @param Profiler $profiler The profiler of the current call + */ + public function __construct(LoggerInterface $logger, Profiler $profiler) + { + $this->logger = $logger; + $this->profiler = $profiler; + } + + /** + * {@inheritdoc} + */ + public function emergency($message, array $context = []) + { + $this->profiler->startRecording('file'); + $this->logger->emergency($message, $context); + $this->profiler->stopRecording(); + } + + /** + * {@inheritdoc} + */ + public function alert($message, array $context = []) + { + $this->profiler->startRecording('file'); + $this->logger->alert($message, $context); + $this->profiler->stopRecording(); + } + + /** + * {@inheritdoc} + */ + public function critical($message, array $context = []) + { + $this->profiler->startRecording('file'); + $this->logger->critical($message, $context); + $this->profiler->stopRecording(); + } + + /** + * {@inheritdoc} + */ + public function error($message, array $context = []) + { + $this->profiler->startRecording('file'); + $this->logger->error($message, $context); + $this->profiler->stopRecording(); + } + + /** + * {@inheritdoc} + */ + public function warning($message, array $context = []) + { + $this->profiler->startRecording('file'); + $this->logger->warning($message, $context); + $this->profiler->stopRecording(); + } + + /** + * {@inheritdoc} + */ + public function notice($message, array $context = []) + { + $this->profiler->startRecording('file'); + $this->logger->notice($message, $context); + $this->profiler->stopRecording(); + } + + /** + * {@inheritdoc} + */ + public function info($message, array $context = []) + { + $this->profiler->startRecording('file'); + $this->logger->info($message, $context); + $this->profiler->stopRecording(); + } + + /** + * {@inheritdoc} + */ + public function debug($message, array $context = []) + { + $this->profiler->startRecording('file'); + $this->logger->debug($message, $context); + $this->profiler->stopRecording(); + } + + /** + * {@inheritdoc} + */ + public function log($level, $message, array $context = []) + { + $this->profiler->startRecording('file'); + $this->logger->log($level, $message, $context); + $this->profiler->stopRecording(); + } +} diff --git a/src/Core/Logger/Type/README.md b/src/Core/Logger/Type/README.md new file mode 100644 index 0000000000..449403194d --- /dev/null +++ b/src/Core/Logger/Type/README.md @@ -0,0 +1,27 @@ +## Friendica\Util\Logger + +This namespace contains the different implementations of a Logger. + +### Configuration guideline + +The following settings are possible for `logger_config`: +- `monolog`: A Logging framework with lots of additions (see [Monolog](https://github.com/Seldaek/monolog/)). There are just Friendica additions inside the Monolog directory +- [`stream`](StreamLogger.php): A small logger for files or streams +- [`syslog`](SyslogLogger.php): Prints the logging output into the syslog + +[`VoidLogger`](VoidLogger.php) is a fallback logger without any function if no debugging is enabled. + +[`ProfilerLogger`](ProfilerLogger.php) is a wrapper around an existing logger in case profiling is enabled for Friendica. +Every log call will be saved to the `Profiler` with a timestamp. + +### Implementation guideline + +Each logging implementation should pe capable of printing at least the following information: +- An unique ID for each Request/Call +- The process ID (PID) +- A timestamp of the logging entry +- The critically of the log entry +- A log message +- A context of the log message (f.e which user) + +If possible, a Logger should extend [`AbstractLogger`](AbstractLogger.php), because it contains additional, Friendica specific business logic for each logging call. diff --git a/src/Core/Logger/Type/StreamLogger.php b/src/Core/Logger/Type/StreamLogger.php new file mode 100644 index 0000000000..be0283d0ec --- /dev/null +++ b/src/Core/Logger/Type/StreamLogger.php @@ -0,0 +1,203 @@ +. + * + */ + +namespace Friendica\Core\Logger\Type; + +use Friendica\Core\Logger\Exception\LoggerArgumentException; +use Friendica\Core\Logger\Exception\LoggerException; +use Friendica\Util\DateTimeFormat; +use Friendica\Util\FileSystem; +use Friendica\Util\Introspection; +use Psr\Log\LogLevel; + +/** + * A Logger instance for logging into a stream (file, stdout, stderr) + */ +class StreamLogger extends AbstractLogger +{ + /** + * The minimum loglevel at which this logger will be triggered + * @var string + */ + private $logLevel; + + /** + * The file URL of the stream (if needed) + * @var string + */ + private $url; + + /** + * The stream, where the current logger is writing into + * @var resource + */ + private $stream; + + /** + * The current process ID + * @var int + */ + private $pid; + + /** + * @var FileSystem + */ + private $fileSystem; + + /** + * Translates LogLevel log levels to integer values + * @var array + */ + private $levelToInt = [ + LogLevel::EMERGENCY => 0, + LogLevel::ALERT => 1, + LogLevel::CRITICAL => 2, + LogLevel::ERROR => 3, + LogLevel::WARNING => 4, + LogLevel::NOTICE => 5, + LogLevel::INFO => 6, + LogLevel::DEBUG => 7, + ]; + + /** + * {@inheritdoc} + * @param string|resource $stream The stream to write with this logger (either a file or a stream, i.e. stdout) + * @param string $level The minimum loglevel at which this logger will be triggered + * + * @throws LoggerArgumentException + */ + public function __construct($channel, $stream, Introspection $introspection, FileSystem $fileSystem, string $level = LogLevel::DEBUG) + { + $this->fileSystem = $fileSystem; + + parent::__construct($channel, $introspection); + + if (is_resource($stream)) { + $this->stream = $stream; + } elseif (is_string($stream)) { + $this->url = $stream; + } else { + throw new LoggerArgumentException('A stream must either be a resource or a string.'); + } + + $this->pid = getmypid(); + if (array_key_exists($level, $this->levelToInt)) { + $this->logLevel = $this->levelToInt[$level]; + } else { + throw new LoggerArgumentException(sprintf('The level "%s" is not valid.', $level)); + } + + $this->checkStream(); + } + + public function close() + { + if ($this->url && is_resource($this->stream)) { + fclose($this->stream); + } + + $this->stream = null; + } + + /** + * Adds a new entry to the log + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + * + * @throws LoggerException + * @throws LoggerArgumentException + */ + protected function addEntry($level, string $message, array $context = []) + { + if (!array_key_exists($level, $this->levelToInt)) { + throw new LoggerArgumentException(sprintf('The level "%s" is not valid.', $level)); + } + + $logLevel = $this->levelToInt[$level]; + + if ($logLevel > $this->logLevel) { + return; + } + + $this->checkStream(); + + $formattedLog = $this->formatLog($level, $message, $context); + fwrite($this->stream, $formattedLog); + } + + /** + * Formats a log record for the syslog output + * + * @param mixed $level The loglevel/priority + * @param string $message The message + * @param array $context The context of this call + * + * @return string the formatted syslog output + * + * @throws LoggerException + */ + private function formatLog($level, string $message, array $context = []): string + { + $record = $this->introspection->getRecord(); + $record = array_merge($record, ['uid' => $this->logUid, 'process_id' => $this->pid]); + + try { + $logMessage = DateTimeFormat::utcNow(DateTimeFormat::ATOM) . ' '; + } catch (\Exception $exception) { + throw new LoggerException('Cannot get current datetime.', $exception); + } + $logMessage .= $this->channel . ' '; + $logMessage .= '[' . strtoupper($level) . ']: '; + $logMessage .= $this->psrInterpolate($message, $context) . ' '; + $logMessage .= $this->jsonEncodeArray($context) . ' - '; + $logMessage .= $this->jsonEncodeArray($record); + $logMessage .= PHP_EOL; + + return $logMessage; + } + + /** + * Checks the current stream + * + * @throws LoggerException + * @throws LoggerArgumentException + */ + private function checkStream() + { + if (is_resource($this->stream)) { + return; + } + + if (empty($this->url)) { + throw new LoggerArgumentException('Missing stream URL.'); + } + + try { + $this->stream = $this->fileSystem->createStream($this->url); + } catch (\UnexpectedValueException $exception) { + throw new LoggerException('Cannot create stream.', $exception); + } + } +} diff --git a/src/Core/Logger/Type/SyslogLogger.php b/src/Core/Logger/Type/SyslogLogger.php new file mode 100644 index 0000000000..667b44ccc7 --- /dev/null +++ b/src/Core/Logger/Type/SyslogLogger.php @@ -0,0 +1,230 @@ +. + * + */ + +namespace Friendica\Core\Logger\Type; + +use Friendica\Core\Logger\Exception\LoggerArgumentException; +use Friendica\Core\Logger\Exception\LoggerException; +use Friendica\Util\Introspection; +use Psr\Log\InvalidArgumentException; +use Psr\Log\LogLevel; + +/** + * A Logger instance for syslogging (fast, but simple) + * @see http://php.net/manual/en/function.syslog.php + */ +class SyslogLogger extends AbstractLogger +{ + const IDENT = 'Friendica'; + + /** + * Translates LogLevel log levels to syslog log priorities. + * @var array + */ + private $logLevels = [ + LogLevel::DEBUG => LOG_DEBUG, + LogLevel::INFO => LOG_INFO, + LogLevel::NOTICE => LOG_NOTICE, + LogLevel::WARNING => LOG_WARNING, + LogLevel::ERROR => LOG_ERR, + LogLevel::CRITICAL => LOG_CRIT, + LogLevel::ALERT => LOG_ALERT, + LogLevel::EMERGENCY => LOG_EMERG, + ]; + + /** + * Translates log priorities to string outputs + * @var array + */ + private $logToString = [ + LOG_DEBUG => 'DEBUG', + LOG_INFO => 'INFO', + LOG_NOTICE => 'NOTICE', + LOG_WARNING => 'WARNING', + LOG_ERR => 'ERROR', + LOG_CRIT => 'CRITICAL', + LOG_ALERT => 'ALERT', + LOG_EMERG => 'EMERGENCY' + ]; + + /** + * Indicates what logging options will be used when generating a log message + * @see http://php.net/manual/en/function.openlog.php#refsect1-function.openlog-parameters + * + * @var int + */ + private $logOpts; + + /** + * Used to specify what type of program is logging the message + * @see http://php.net/manual/en/function.openlog.php#refsect1-function.openlog-parameters + * + * @var int + */ + private $logFacility; + + /** + * The minimum loglevel at which this logger will be triggered + * @var int + */ + private $logLevel; + + /** + * A error message of the current operation + * @var string + */ + private $errorMessage; + + /** + * {@inheritdoc} + * @param string $level The minimum loglevel at which this logger will be triggered + * @param int $logOpts Indicates what logging options will be used when generating a log message + * @param int $logFacility Used to specify what type of program is logging the message + * + * @throws LoggerArgumentException + */ + public function __construct($channel, Introspection $introspection, string $level = LogLevel::NOTICE, int $logOpts = LOG_PID, int $logFacility = LOG_USER) + { + parent::__construct($channel, $introspection); + $this->logOpts = $logOpts; + $this->logFacility = $logFacility; + $this->logLevel = $this->mapLevelToPriority($level); + $this->introspection->addClasses([self::class]); + } + + /** + * Adds a new entry to the syslog + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @throws LoggerArgumentException in case the level isn't valid + * @throws LoggerException In case the syslog cannot be opened for writing + */ + protected function addEntry($level, string $message, array $context = []) + { + $logLevel = $this->mapLevelToPriority($level); + + if ($logLevel > $this->logLevel) { + return; + } + + $formattedLog = $this->formatLog($logLevel, $message, $context); + $this->write($logLevel, $formattedLog); + } + + /** + * Maps the LogLevel (@see LogLevel) to a SysLog priority (@see http://php.net/manual/en/function.syslog.php#refsect1-function.syslog-parameters) + * + * @param string $level A LogLevel + * + * @return int The SysLog priority + * + * @throws LoggerArgumentException If the loglevel isn't valid + */ + public function mapLevelToPriority(string $level): int + { + if (!array_key_exists($level, $this->logLevels)) { + throw new LoggerArgumentException(sprintf('The level "%s" is not valid.', $level)); + } + + return $this->logLevels[$level]; + } + + /** + * Closes the Syslog + */ + public function close() + { + closelog(); + } + + /** + * Writes a message to the syslog + * + * @see http://php.net/manual/en/function.syslog.php#refsect1-function.syslog-parameters + * + * @param int $priority The Priority + * @param string $message The message of the log + * + * @throws LoggerException In case the syslog cannot be opened/written + */ + private function write(int $priority, string $message) + { + set_error_handler([$this, 'customErrorHandler']); + $opened = openlog(self::IDENT, $this->logOpts, $this->logFacility); + restore_error_handler(); + + if (!$opened) { + throw new LoggerException(sprintf('Can\'t open syslog for ident "%s" and facility "%s": ' . $this->errorMessage, $this->channel, $this->logFacility)); + } + + $this->syslogWrapper($priority, $message); + } + + /** + * Formats a log record for the syslog output + * + * @param int $level The loglevel/priority + * @param string $message The message + * @param array $context The context of this call + * + * @return string the formatted syslog output + */ + private function formatLog(int $level, string $message, array $context = []): string + { + $record = $this->introspection->getRecord(); + $record = array_merge($record, ['uid' => $this->logUid]); + + $logMessage = $this->channel . ' '; + $logMessage .= '[' . $this->logToString[$level] . ']: '; + $logMessage .= $this->psrInterpolate($message, $context) . ' '; + $logMessage .= $this->jsonEncodeArray($context) . ' - '; + $logMessage .= $this->jsonEncodeArray($record); + + return $logMessage; + } + + private function customErrorHandler($code, $msg) + { + $this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $msg); + } + + /** + * A syslog wrapper to make syslog functionality testable + * + * @param int $level The syslog priority + * @param string $entry The message to send to the syslog function + * + * @throws LoggerException + */ + protected function syslogWrapper(int $level, string $entry) + { + set_error_handler([$this, 'customErrorHandler']); + $written = syslog($level, $entry); + restore_error_handler(); + + if (!$written) { + throw new LoggerException(sprintf('Can\'t write into syslog for ident "%s" and facility "%s": ' . $this->errorMessage, $this->channel, $this->logFacility)); + } + } +} diff --git a/src/Core/Logger/Type/VoidLogger.php b/src/Core/Logger/Type/VoidLogger.php new file mode 100644 index 0000000000..5cd2cac868 --- /dev/null +++ b/src/Core/Logger/Type/VoidLogger.php @@ -0,0 +1,159 @@ +. + * + */ + +namespace Friendica\Core\Logger\Type; + +use Psr\Log\LoggerInterface; + +/** + * A Logger instance to not log + */ +class VoidLogger implements LoggerInterface +{ + /** + * System is unusable. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function emergency($message, array $context = array()) + { + return; + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function alert($message, array $context = array()) + { + return; + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function critical($message, array $context = array()) + { + return; + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function error($message, array $context = array()) + { + return; + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + return; + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function notice($message, array $context = array()) + { + return; + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function info($message, array $context = array()) + { + return; + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + return; + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + */ + public function log($level, $message, array $context = array()) + { + return; + } +} diff --git a/src/Core/Logger/Type/WorkerLogger.php b/src/Core/Logger/Type/WorkerLogger.php new file mode 100644 index 0000000000..fcbe128013 --- /dev/null +++ b/src/Core/Logger/Type/WorkerLogger.php @@ -0,0 +1,235 @@ +. + * + */ + +namespace Friendica\Core\Logger\Type; + +use Friendica\Core\Logger\Exception\LoggerException; +use Friendica\Util\Strings; +use Psr\Log\LoggerInterface; + +/** + * A Logger for specific worker tasks, which adds a worker id to it. + * Uses the decorator pattern (https://en.wikipedia.org/wiki/Decorator_pattern) + */ +class WorkerLogger implements LoggerInterface +{ + /** + * @var LoggerInterface The original Logger instance + */ + private $logger; + + /** + * @var string the current worker ID + */ + private $workerId; + + /** + * @var string The called function name + */ + private $functionName; + + /** + * @param LoggerInterface $logger The logger for worker entries + * @param string $functionName The current function name of the worker + * @param int $idLength The length of the generated worker ID + * + * @throws LoggerException + */ + public function __construct(LoggerInterface $logger, string $functionName = '', int $idLength = 7) + { + $this->logger = $logger; + $this->functionName = $functionName; + try { + $this->workerId = Strings::getRandomHex($idLength); + } catch (\Exception $exception) { + throw new LoggerException('Cannot generate random Hex.', $exception); + } + } + + /** + * Sets the function name for additional logging + * + * @param string $functionName + */ + public function setFunctionName(string $functionName) + { + $this->functionName = $functionName; + } + + /** + * Adds the worker context for each log entry + * + * @param array $context + */ + private function addContext(array &$context) + { + $context['worker_id'] = $this->workerId; + $context['worker_cmd'] = $this->functionName; + } + + /** + * Returns the worker ID + * + * @return string + */ + public function getWorkerId(): string + { + return $this->workerId; + } + + /** + * System is unusable. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function emergency($message, array $context = []) + { + $this->addContext($context); + $this->logger->emergency($message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function alert($message, array $context = []) + { + $this->addContext($context); + $this->logger->alert($message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function critical($message, array $context = []) + { + $this->addContext($context); + $this->logger->critical($message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function error($message, array $context = []) + { + $this->addContext($context); + $this->logger->error($message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function warning($message, array $context = []) + { + $this->addContext($context); + $this->logger->warning($message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function notice($message, array $context = []) + { + $this->addContext($context); + $this->logger->notice($message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function info($message, array $context = []) + { + $this->addContext($context); + $this->logger->info($message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function debug($message, array $context = []) + { + $this->addContext($context); + $this->logger->debug($message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + */ + public function log($level, $message, array $context = []) + { + $this->addContext($context); + $this->logger->log($level, $message, $context); + } +} diff --git a/src/DI.php b/src/DI.php index 5ba7e88db1..8ee7004408 100644 --- a/src/DI.php +++ b/src/DI.php @@ -243,7 +243,7 @@ abstract class DI */ public static function workerLogger() { - return self::$dice->create(Util\Logger\WorkerLogger::class); + return self::$dice->create(Core\Logger\Type\WorkerLogger::class); } // diff --git a/src/Factory/LoggerFactory.php b/src/Factory/LoggerFactory.php deleted file mode 100644 index d01b477e1e..0000000000 --- a/src/Factory/LoggerFactory.php +++ /dev/null @@ -1,293 +0,0 @@ -. - * - */ - -namespace Friendica\Factory; - -use Friendica\Core\Config\Capability\IManageConfigValues; -use Friendica\Core\Logger; -use Friendica\Database\Database; -use Friendica\Network\HTTPException\InternalServerErrorException; -use Friendica\Util\FileSystem; -use Friendica\Util\Introspection; -use Friendica\Util\Logger\Monolog\DevelopHandler; -use Friendica\Util\Logger\Monolog\IntrospectionProcessor; -use Friendica\Util\Logger\ProfilerLogger; -use Friendica\Util\Logger\StreamLogger; -use Friendica\Util\Logger\SyslogLogger; -use Friendica\Util\Logger\VoidLogger; -use Friendica\Util\Profiler; -use Monolog; -use Psr\Log\LoggerInterface; -use Psr\Log\LogLevel; - -/** - * A logger factory - * - * Currently only Monolog is supported - */ -class LoggerFactory -{ - const DEV_CHANNEL = 'dev'; - - /** - * A list of classes, which shouldn't get logged - * - * @var array - */ - private static $ignoreClassList = [ - Logger::class, - Profiler::class, - 'Friendica\\Util\\Logger', - ]; - - private $channel; - - public function __construct(string $channel) - { - $this->channel = $channel; - } - - /** - * Creates a new PSR-3 compliant logger instances - * - * @param Database $database The Friendica Database instance - * @param IManageConfigValues $config The config - * @param Profiler $profiler The profiler of the app - * @param FileSystem $fileSystem FileSystem utils - * - * @return LoggerInterface The PSR-3 compliant logger instance - */ - public function create(Database $database, IManageConfigValues $config, Profiler $profiler, FileSystem $fileSystem) - { - if (empty($config->get('system', 'debugging', false))) { - $logger = new VoidLogger(); - $database->setLogger($logger); - return $logger; - } - - $introspection = new Introspection(self::$ignoreClassList); - $level = $config->get('system', 'loglevel'); - $loglevel = self::mapLegacyConfigDebugLevel((string)$level); - - switch ($config->get('system', 'logger_config', 'stream')) { - case 'monolog': - $loggerTimeZone = new \DateTimeZone('UTC'); - Monolog\Logger::setTimezone($loggerTimeZone); - - $logger = new Monolog\Logger($this->channel); - $logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor()); - $logger->pushProcessor(new Monolog\Processor\ProcessIdProcessor()); - $logger->pushProcessor(new Monolog\Processor\UidProcessor()); - $logger->pushProcessor(new IntrospectionProcessor($introspection, LogLevel::DEBUG)); - - $stream = $config->get('system', 'logfile'); - - // just add a stream in case it's either writable or not file - if (!is_file($stream) || is_writable($stream)) { - try { - static::addStreamHandler($logger, $stream, $loglevel); - } catch (\Throwable $e) { - // No Logger .. - $logger = new VoidLogger(); - } - } - break; - - case 'syslog': - try { - $logger = new SyslogLogger($this->channel, $introspection, $loglevel); - } catch (\Throwable $e) { - // No logger ... - $logger = new VoidLogger(); - } - break; - - case 'stream': - default: - $stream = $config->get('system', 'logfile'); - // just add a stream in case it's either writable or not file - if (!is_file($stream) || is_writable($stream)) { - try { - $logger = new StreamLogger($this->channel, $stream, $introspection, $fileSystem, $loglevel); - } catch (\Throwable $t) { - // No logger ... - $logger = new VoidLogger(); - } - } else { - $logger = new VoidLogger(); - } - break; - } - - $profiling = $config->get('system', 'profiling', false); - - // In case profiling is enabled, wrap the ProfilerLogger around the current logger - if (isset($profiling) && $profiling !== false) { - $logger = new ProfilerLogger($logger, $profiler); - } - - $database->setLogger($logger); - return $logger; - } - - /** - * Creates a new PSR-3 compliant develop logger - * - * If you want to debug only interactions from your IP or the IP of a remote server for federation debug, - * you'll use this logger instance for the duration of your work. - * - * It should never get filled during normal usage of Friendica - * - * @param IManageConfigValues $config The config - * @param Profiler $profiler The profiler of the app - * @param FileSystem $fileSystem FileSystem utils - * - * @return LoggerInterface The PSR-3 compliant logger instance - * - * @throws InternalServerErrorException - * @throws \Exception - */ - public static function createDev(IManageConfigValues $config, Profiler $profiler, FileSystem $fileSystem) - { - $debugging = $config->get('system', 'debugging'); - $stream = $config->get('system', 'dlogfile'); - $developerIp = $config->get('system', 'dlogip'); - - if ((!isset($developerIp) || !$debugging) && - (!is_file($stream) || is_writable($stream))) { - $logger = new VoidLogger(); - return $logger; - } - - $loggerTimeZone = new \DateTimeZone('UTC'); - Monolog\Logger::setTimezone($loggerTimeZone); - - $introspection = new Introspection(self::$ignoreClassList); - - switch ($config->get('system', 'logger_config', 'stream')) { - - case 'monolog': - $loggerTimeZone = new \DateTimeZone('UTC'); - Monolog\Logger::setTimezone($loggerTimeZone); - - $logger = new Monolog\Logger(self::DEV_CHANNEL); - $logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor()); - $logger->pushProcessor(new Monolog\Processor\ProcessIdProcessor()); - $logger->pushProcessor(new Monolog\Processor\UidProcessor()); - $logger->pushProcessor(new IntrospectionProcessor($introspection, LogLevel::DEBUG)); - - $logger->pushHandler(new DevelopHandler($developerIp)); - - static::addStreamHandler($logger, $stream, LogLevel::DEBUG); - break; - - case 'syslog': - $logger = new SyslogLogger(self::DEV_CHANNEL, $introspection, LogLevel::DEBUG); - break; - - case 'stream': - default: - $logger = new StreamLogger(self::DEV_CHANNEL, $stream, $introspection, $fileSystem, LogLevel::DEBUG); - break; - } - - $profiling = $config->get('system', 'profiling', false); - - // In case profiling is enabled, wrap the ProfilerLogger around the current logger - if (isset($profiling) && $profiling !== false) { - $logger = new ProfilerLogger($logger, $profiler); - } - - return $logger; - } - - /** - * Mapping a legacy level to the PSR-3 compliant levels - * - * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md#5-psrlogloglevel - * - * @param string $level the level to be mapped - * - * @return string the PSR-3 compliant level - */ - private static function mapLegacyConfigDebugLevel($level) - { - switch ($level) { - // legacy WARNING - case "0": - return LogLevel::ERROR; - // legacy INFO - case "1": - return LogLevel::WARNING; - // legacy TRACE - case "2": - return LogLevel::NOTICE; - // legacy DEBUG - case "3": - return LogLevel::INFO; - // legacy DATA - case "4": - // legacy ALL - case "5": - return LogLevel::DEBUG; - // default if nothing set - default: - return $level; - } - } - - /** - * Adding a handler to a given logger instance - * - * @param LoggerInterface $logger The logger instance - * @param mixed $stream The stream which handles the logger output - * @param string $level The level, for which this handler at least should handle logging - * - * @return void - * - * @throws \Exception in case of general failures - */ - public static function addStreamHandler($logger, $stream, $level = LogLevel::NOTICE) - { - if ($logger instanceof Monolog\Logger) { - $loglevel = Monolog\Logger::toMonologLevel($level); - - // fallback to notice if an invalid loglevel is set - if (!is_int($loglevel)) { - $loglevel = LogLevel::NOTICE; - } - - $fileHandler = new Monolog\Handler\StreamHandler($stream, $loglevel); - - $formatter = new Monolog\Formatter\LineFormatter("%datetime% %channel% [%level_name%]: %message% %context% %extra%\n"); - $fileHandler->setFormatter($formatter); - - $logger->pushHandler($fileHandler); - } - } - - public static function addVoidHandler($logger) - { - if ($logger instanceof Monolog\Logger) { - $logger->pushHandler(new Monolog\Handler\NullHandler()); - } - } -} diff --git a/src/Util/FileSystem.php b/src/Util/FileSystem.php index 4fa5c31c8a..1a86f43be7 100644 --- a/src/Util/FileSystem.php +++ b/src/Util/FileSystem.php @@ -73,7 +73,9 @@ class FileSystem * * @param string $url The file/url * - * @return false|resource the open stream ressource + * @return resource the open stream rssource + * + * @throws \UnexpectedValueException */ public function createStream(string $url) { diff --git a/src/Util/Logger/AbstractLogger.php b/src/Util/Logger/AbstractLogger.php deleted file mode 100644 index a8aba34d34..0000000000 --- a/src/Util/Logger/AbstractLogger.php +++ /dev/null @@ -1,199 +0,0 @@ -. - * - */ - -namespace Friendica\Util\Logger; - -use Friendica\Util\Introspection; -use Friendica\Util\Strings; -use Psr\Log\LoggerInterface; -use Psr\Log\LogLevel; - -/** - * This class contains all necessary dependencies and calls for Friendica - * Every new Logger should extend this class and define, how addEntry() works - * - * Additional information for each Logger, who extends this class: - * - Introspection - * - UID for each call - * - Channel of the current call (i.e. index, worker, daemon, ...) - */ -abstract class AbstractLogger implements LoggerInterface -{ - /** - * The output channel of this logger - * @var string - */ - protected $channel; - - /** - * The Introspection for the current call - * @var Introspection - */ - protected $introspection; - - /** - * The UID of the current call - * @var string - */ - protected $logUid; - - /** - * Adds a new entry to the log - * - * @param int $level - * @param string $message - * @param array $context - * - * @return void - */ - abstract protected function addEntry($level, $message, $context = []); - - /** - * @param string $channel The output channel - * @param Introspection $introspection The introspection of the current call - * - * @throws \Exception - */ - public function __construct($channel, Introspection $introspection) - { - $this->channel = $channel; - $this->introspection = $introspection; - $this->logUid = Strings::getRandomHex(6); - } - - /** - * Simple interpolation of PSR-3 compliant replacements ( variables between '{' and '}' ) - * @see https://www.php-fig.org/psr/psr-3/#12-message - * - * @param string $message - * @param array $context - * - * @return string the interpolated message - */ - protected function psrInterpolate($message, array $context = array()) - { - $replace = []; - foreach ($context as $key => $value) { - // check that the value can be casted to string - if (!is_array($value) && (!is_object($value) || method_exists($value, '__toString'))) { - $replace['{' . $key . '}'] = $value; - } elseif (is_array($value)) { - $replace['{' . $key . '}'] = @json_encode($value); - } - } - - return strtr($message, $replace); - } - - /** - * JSON Encodes an complete array including objects with "__toString()" methods - * - * @param array $input an Input Array to encode - * - * @return false|string The json encoded output of the array - */ - protected function jsonEncodeArray(array $input) - { - $output = []; - - foreach ($input as $key => $value) { - if (is_object($value) && method_exists($value, '__toString')) { - $output[$key] = $value->__toString(); - } else { - $output[$key] = $value; - } - } - - return @json_encode($output); - } - - /** - * {@inheritdoc} - */ - public function emergency($message, array $context = array()) - { - $this->addEntry(LogLevel::EMERGENCY, (string) $message, $context); - } - - /** - * {@inheritdoc} - */ - public function alert($message, array $context = array()) - { - $this->addEntry(LogLevel::ALERT, (string) $message, $context); - } - - /** - * {@inheritdoc} - */ - public function critical($message, array $context = array()) - { - $this->addEntry(LogLevel::CRITICAL, (string) $message, $context); - } - - /** - * {@inheritdoc} - */ - public function error($message, array $context = array()) - { - $this->addEntry(LogLevel::ERROR, (string) $message, $context); - } - - /** - * {@inheritdoc} - */ - public function warning($message, array $context = array()) - { - $this->addEntry(LogLevel::WARNING, (string) $message, $context); - } - - /** - * {@inheritdoc} - */ - public function notice($message, array $context = array()) - { - $this->addEntry(LogLevel::NOTICE, (string) $message, $context); - } - - /** - * {@inheritdoc} - */ - public function info($message, array $context = array()) - { - $this->addEntry(LogLevel::INFO, (string) $message, $context); - } - - /** - * {@inheritdoc} - */ - public function debug($message, array $context = array()) - { - $this->addEntry(LogLevel::DEBUG, (string) $message, $context); - } - - /** - * {@inheritdoc} - */ - public function log($level, $message, array $context = array()) - { - $this->addEntry($level, (string) $message, $context); - } -} diff --git a/src/Util/Logger/Monolog/DevelopHandler.php b/src/Util/Logger/Monolog/DevelopHandler.php deleted file mode 100644 index a55ac373ac..0000000000 --- a/src/Util/Logger/Monolog/DevelopHandler.php +++ /dev/null @@ -1,69 +0,0 @@ -. - * - */ - -namespace Friendica\Util\Logger\Monolog; - -use Monolog\Handler; -use Monolog\Logger; - -/** - * Simple handler for Friendica developers to use for deeper logging - * - * If you want to debug only interactions from your IP or the IP of a remote server for federation debug, - * you'll use Logger::develop() for the duration of your work, and you clean it up when you're done before submitting your PR. - */ -class DevelopHandler extends Handler\AbstractHandler -{ - /** - * @var string The IP of the developer who wants to debug - */ - private $developerIp; - - /** - * @param string $developerIp The IP of the developer who wants to debug - * @param int $level The minimum logging level at which this handler will be triggered - * @param bool $bubble Whether the messages that are handled can bubble up the stack or not - */ - public function __construct($developerIp, $level = Logger::DEBUG, $bubble = true) - { - parent::__construct($level, $bubble); - - $this->developerIp = $developerIp; - } - - /** - * {@inheritdoc} - */ - public function handle(array $record) - { - if (!$this->isHandling($record)) { - return false; - } - - /// Just in case the remote IP is the same as the developer IP log the output - if (!is_null($this->developerIp) && $_SERVER['REMOTE_ADDR'] != $this->developerIp) - { - return false; - } - - return false === $this->bubble; - } -} diff --git a/src/Util/Logger/Monolog/IntrospectionProcessor.php b/src/Util/Logger/Monolog/IntrospectionProcessor.php deleted file mode 100644 index ceb0e3123e..0000000000 --- a/src/Util/Logger/Monolog/IntrospectionProcessor.php +++ /dev/null @@ -1,62 +0,0 @@ -. - * - */ - -namespace Friendica\Util\Logger\Monolog; - -use Friendica\Util\Introspection; -use Monolog\Logger; -use Monolog\Processor\ProcessorInterface; - -/** - * Injects line/file//function where the log message came from - */ -class IntrospectionProcessor implements ProcessorInterface -{ - private $level; - - private $introspection; - - /** - * @param Introspection $introspection Holds the Introspection of the current call - * @param string|int $level The minimum logging level at which this Processor will be triggered - */ - public function __construct(Introspection $introspection, $level = Logger::DEBUG) - { - $this->level = Logger::toMonologLevel($level); - $introspection->addClasses(array('Monolog\\')); - $this->introspection = $introspection; - } - - public function __invoke(array $record) - { - // return if the level is not high enough - if ($record['level'] < $this->level) { - return $record; - } - // we should have the call source now - $record['extra'] = array_merge( - $record['extra'], - $this->introspection->getRecord() - ); - - return $record; - } -} diff --git a/src/Util/Logger/ProfilerLogger.php b/src/Util/Logger/ProfilerLogger.php deleted file mode 100644 index 1c190d4f55..0000000000 --- a/src/Util/Logger/ProfilerLogger.php +++ /dev/null @@ -1,146 +0,0 @@ -. - * - */ - -namespace Friendica\Util\Logger; - -use Friendica\Core\System; -use Friendica\Util\Profiler; -use Psr\Log\LoggerInterface; - -/** - * This Logger adds additional profiling data in case profiling is enabled. - * It uses a predefined logger. - */ -class ProfilerLogger implements LoggerInterface -{ - /** - * The Logger of the current call - * @var LoggerInterface - */ - private $logger; - - /** - * The Profiler for the current call - * @var Profiler - */ - protected $profiler; - - /** - * ProfilerLogger constructor. - * @param LoggerInterface $logger The Logger of the current call - * @param Profiler $profiler The profiler of the current call - */ - public function __construct(LoggerInterface $logger, Profiler $profiler) - { - $this->logger = $logger; - $this->profiler = $profiler; - } - - /** - * {@inheritdoc} - */ - public function emergency($message, array $context = array()) - { - $this->profiler->startRecording('file'); - $this->logger->emergency($message, $context); - $this->profiler->stopRecording(); - } - - /** - * {@inheritdoc} - */ - public function alert($message, array $context = array()) - { - $this->profiler->startRecording('file'); - $this->logger->alert($message, $context); - $this->profiler->stopRecording(); - } - - /** - * {@inheritdoc} - */ - public function critical($message, array $context = array()) - { - $this->profiler->startRecording('file'); - $this->logger->critical($message, $context); - $this->profiler->stopRecording(); - } - - /** - * {@inheritdoc} - */ - public function error($message, array $context = array()) - { - $this->profiler->startRecording('file'); - $this->logger->error($message, $context); - $this->profiler->stopRecording(); - } - - /** - * {@inheritdoc} - */ - public function warning($message, array $context = array()) - { - $this->profiler->startRecording('file'); - $this->logger->warning($message, $context); - $this->profiler->stopRecording(); - } - - /** - * {@inheritdoc} - */ - public function notice($message, array $context = array()) - { - $this->profiler->startRecording('file'); - $this->logger->notice($message, $context); - $this->profiler->stopRecording(); - } - - /** - * {@inheritdoc} - */ - public function info($message, array $context = array()) - { - $this->profiler->startRecording('file'); - $this->logger->info($message, $context); - $this->profiler->stopRecording(); - } - - /** - * {@inheritdoc} - */ - public function debug($message, array $context = array()) - { - $this->profiler->startRecording('file'); - $this->logger->debug($message, $context); - $this->profiler->stopRecording(); - } - - /** - * {@inheritdoc} - */ - public function log($level, $message, array $context = array()) - { - $this->profiler->startRecording('file'); - $this->logger->log($level, $message, $context); - $this->profiler->stopRecording(); - } -} diff --git a/src/Util/Logger/README.md b/src/Util/Logger/README.md deleted file mode 100644 index 449403194d..0000000000 --- a/src/Util/Logger/README.md +++ /dev/null @@ -1,27 +0,0 @@ -## Friendica\Util\Logger - -This namespace contains the different implementations of a Logger. - -### Configuration guideline - -The following settings are possible for `logger_config`: -- `monolog`: A Logging framework with lots of additions (see [Monolog](https://github.com/Seldaek/monolog/)). There are just Friendica additions inside the Monolog directory -- [`stream`](StreamLogger.php): A small logger for files or streams -- [`syslog`](SyslogLogger.php): Prints the logging output into the syslog - -[`VoidLogger`](VoidLogger.php) is a fallback logger without any function if no debugging is enabled. - -[`ProfilerLogger`](ProfilerLogger.php) is a wrapper around an existing logger in case profiling is enabled for Friendica. -Every log call will be saved to the `Profiler` with a timestamp. - -### Implementation guideline - -Each logging implementation should pe capable of printing at least the following information: -- An unique ID for each Request/Call -- The process ID (PID) -- A timestamp of the logging entry -- The critically of the log entry -- A log message -- A context of the log message (f.e which user) - -If possible, a Logger should extend [`AbstractLogger`](AbstractLogger.php), because it contains additional, Friendica specific business logic for each logging call. diff --git a/src/Util/Logger/StreamLogger.php b/src/Util/Logger/StreamLogger.php deleted file mode 100644 index 752f486583..0000000000 --- a/src/Util/Logger/StreamLogger.php +++ /dev/null @@ -1,183 +0,0 @@ -. - * - */ - -namespace Friendica\Util\Logger; - -use Friendica\Util\DateTimeFormat; -use Friendica\Util\FileSystem; -use Friendica\Util\Introspection; -use Psr\Log\LogLevel; - -/** - * A Logger instance for logging into a stream (file, stdout, stderr) - */ -class StreamLogger extends AbstractLogger -{ - /** - * The minimum loglevel at which this logger will be triggered - * @var string - */ - private $logLevel; - - /** - * The file URL of the stream (if needed) - * @var string - */ - private $url; - - /** - * The stream, where the current logger is writing into - * @var resource - */ - private $stream; - - /** - * The current process ID - * @var int - */ - private $pid; - - /** - * @var FileSystem - */ - private $fileSystem; - - /** - * Translates LogLevel log levels to integer values - * @var array - */ - private $levelToInt = [ - LogLevel::EMERGENCY => 0, - LogLevel::ALERT => 1, - LogLevel::CRITICAL => 2, - LogLevel::ERROR => 3, - LogLevel::WARNING => 4, - LogLevel::NOTICE => 5, - LogLevel::INFO => 6, - LogLevel::DEBUG => 7, - ]; - - /** - * {@inheritdoc} - * @param string|resource $stream The stream to write with this logger (either a file or a stream, i.e. stdout) - * @param string $level The minimum loglevel at which this logger will be triggered - * - * @throws \Exception - */ - public function __construct($channel, $stream, Introspection $introspection, FileSystem $fileSystem, $level = LogLevel::DEBUG) - { - $this->fileSystem = $fileSystem; - - parent::__construct($channel, $introspection); - - if (is_resource($stream)) { - $this->stream = $stream; - } elseif (is_string($stream)) { - $this->url = $stream; - } else { - throw new \InvalidArgumentException('A stream must either be a resource or a string.'); - } - - $this->pid = getmypid(); - if (array_key_exists($level, $this->levelToInt)) { - $this->logLevel = $this->levelToInt[$level]; - } else { - throw new \InvalidArgumentException(sprintf('The level "%s" is not valid.', $level)); - } - - $this->checkStream(); - } - - public function close() - { - if ($this->url && is_resource($this->stream)) { - fclose($this->stream); - } - - $this->stream = null; - } - - /** - * Adds a new entry to the log - * - * @param int $level - * @param string $message - * @param array $context - * - * @return void - */ - protected function addEntry($level, $message, $context = []) - { - if (!array_key_exists($level, $this->levelToInt)) { - throw new \InvalidArgumentException(sprintf('The level "%s" is not valid.', $level)); - } - - $logLevel = $this->levelToInt[$level]; - - if ($logLevel > $this->logLevel) { - return; - } - - $this->checkStream(); - - $formattedLog = $this->formatLog($level, $message, $context); - fwrite($this->stream, $formattedLog); - } - - /** - * Formats a log record for the syslog output - * - * @param int $level The loglevel/priority - * @param string $message The message - * @param array $context The context of this call - * - * @return string the formatted syslog output - */ - private function formatLog($level, $message, $context = []) - { - $record = $this->introspection->getRecord(); - $record = array_merge($record, ['uid' => $this->logUid, 'process_id' => $this->pid]); - $logMessage = ''; - - $logMessage .= DateTimeFormat::utcNow(DateTimeFormat::ATOM) . ' '; - $logMessage .= $this->channel . ' '; - $logMessage .= '[' . strtoupper($level) . ']: '; - $logMessage .= $this->psrInterpolate($message, $context) . ' '; - $logMessage .= $this->jsonEncodeArray($context) . ' - '; - $logMessage .= $this->jsonEncodeArray($record); - $logMessage .= PHP_EOL; - - return $logMessage; - } - - private function checkStream() - { - if (is_resource($this->stream)) { - return; - } - - if (empty($this->url)) { - throw new \LogicException('Missing stream URL.'); - } - - $this->stream = $this->fileSystem->createStream($this->url); - } -} diff --git a/src/Util/Logger/SyslogLogger.php b/src/Util/Logger/SyslogLogger.php deleted file mode 100644 index f33e2d624a..0000000000 --- a/src/Util/Logger/SyslogLogger.php +++ /dev/null @@ -1,225 +0,0 @@ -. - * - */ - -namespace Friendica\Util\Logger; - -use Friendica\Network\HTTPException\InternalServerErrorException; -use Friendica\Util\Introspection; -use Psr\Log\LogLevel; - -/** - * A Logger instance for syslogging (fast, but simple) - * @see http://php.net/manual/en/function.syslog.php - */ -class SyslogLogger extends AbstractLogger -{ - const IDENT = 'Friendica'; - - /** - * Translates LogLevel log levels to syslog log priorities. - * @var array - */ - private $logLevels = [ - LogLevel::DEBUG => LOG_DEBUG, - LogLevel::INFO => LOG_INFO, - LogLevel::NOTICE => LOG_NOTICE, - LogLevel::WARNING => LOG_WARNING, - LogLevel::ERROR => LOG_ERR, - LogLevel::CRITICAL => LOG_CRIT, - LogLevel::ALERT => LOG_ALERT, - LogLevel::EMERGENCY => LOG_EMERG, - ]; - - /** - * Translates log priorities to string outputs - * @var array - */ - private $logToString = [ - LOG_DEBUG => 'DEBUG', - LOG_INFO => 'INFO', - LOG_NOTICE => 'NOTICE', - LOG_WARNING => 'WARNING', - LOG_ERR => 'ERROR', - LOG_CRIT => 'CRITICAL', - LOG_ALERT => 'ALERT', - LOG_EMERG => 'EMERGENCY' - ]; - - /** - * Indicates what logging options will be used when generating a log message - * @see http://php.net/manual/en/function.openlog.php#refsect1-function.openlog-parameters - * - * @var int - */ - private $logOpts; - - /** - * Used to specify what type of program is logging the message - * @see http://php.net/manual/en/function.openlog.php#refsect1-function.openlog-parameters - * - * @var int - */ - private $logFacility; - - /** - * The minimum loglevel at which this logger will be triggered - * @var int - */ - private $logLevel; - - /** - * A error message of the current operation - * @var string - */ - private $errorMessage; - - /** - * {@inheritdoc} - * @param string $level The minimum loglevel at which this logger will be triggered - * @param int $logOpts Indicates what logging options will be used when generating a log message - * @param int $logFacility Used to specify what type of program is logging the message - * - * @throws \Exception - */ - public function __construct($channel, Introspection $introspection, $level = LogLevel::NOTICE, $logOpts = LOG_PID, $logFacility = LOG_USER) - { - parent::__construct($channel, $introspection); - $this->logOpts = $logOpts; - $this->logFacility = $logFacility; - $this->logLevel = $this->mapLevelToPriority($level); - $this->introspection->addClasses(array(self::class)); - } - - /** - * Adds a new entry to the syslog - * - * @param int $level - * @param string $message - * @param array $context - * - * @throws InternalServerErrorException if the syslog isn't available - */ - protected function addEntry($level, $message, $context = []) - { - $logLevel = $this->mapLevelToPriority($level); - - if ($logLevel > $this->logLevel) { - return; - } - - $formattedLog = $this->formatLog($logLevel, $message, $context); - $this->write($logLevel, $formattedLog); - } - - /** - * Maps the LogLevel (@see LogLevel) to a SysLog priority (@see http://php.net/manual/en/function.syslog.php#refsect1-function.syslog-parameters) - * - * @param string $level A LogLevel - * - * @return int The SysLog priority - * - * @throws \Psr\Log\InvalidArgumentException If the loglevel isn't valid - */ - public function mapLevelToPriority($level) - { - if (!array_key_exists($level, $this->logLevels)) { - throw new \InvalidArgumentException(sprintf('The level "%s" is not valid.', $level)); - } - - return $this->logLevels[$level]; - } - - /** - * Closes the Syslog - */ - public function close() - { - closelog(); - } - - /** - * Writes a message to the syslog - * @see http://php.net/manual/en/function.syslog.php#refsect1-function.syslog-parameters - * - * @param int $priority The Priority - * @param string $message The message of the log - * - * @throws InternalServerErrorException if syslog cannot be used - */ - private function write($priority, $message) - { - set_error_handler([$this, 'customErrorHandler']); - $opened = openlog(self::IDENT, $this->logOpts, $this->logFacility); - restore_error_handler(); - - if (!$opened) { - throw new \UnexpectedValueException(sprintf('Can\'t open syslog for ident "%s" and facility "%s": ' . $this->errorMessage, $this->channel, $this->logFacility)); - } - - $this->syslogWrapper($priority, $message); - } - - /** - * Formats a log record for the syslog output - * - * @param int $level The loglevel/priority - * @param string $message The message - * @param array $context The context of this call - * - * @return string the formatted syslog output - */ - private function formatLog($level, $message, $context = []) - { - $record = $this->introspection->getRecord(); - $record = array_merge($record, ['uid' => $this->logUid]); - $logMessage = ''; - - $logMessage .= $this->channel . ' '; - $logMessage .= '[' . $this->logToString[$level] . ']: '; - $logMessage .= $this->psrInterpolate($message, $context) . ' '; - $logMessage .= $this->jsonEncodeArray($context) . ' - '; - $logMessage .= $this->jsonEncodeArray($record); - - return $logMessage; - } - - private function customErrorHandler($code, $msg) - { - $this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $msg); - } - - /** - * A syslog wrapper to make syslog functionality testable - * - * @param int $level The syslog priority - * @param string $entry The message to send to the syslog function - */ - protected function syslogWrapper($level, $entry) - { - set_error_handler([$this, 'customErrorHandler']); - $written = syslog($level, $entry); - restore_error_handler(); - - if (!$written) { - throw new \UnexpectedValueException(sprintf('Can\'t write into syslog for ident "%s" and facility "%s": ' . $this->errorMessage, $this->channel, $this->logFacility)); - } - } -} diff --git a/src/Util/Logger/VoidLogger.php b/src/Util/Logger/VoidLogger.php deleted file mode 100644 index 6c47601a0a..0000000000 --- a/src/Util/Logger/VoidLogger.php +++ /dev/null @@ -1,159 +0,0 @@ -. - * - */ - -namespace Friendica\Util\Logger; - -use Psr\Log\LoggerInterface; - -/** - * A Logger instance to not log - */ -class VoidLogger implements LoggerInterface -{ - /** - * System is unusable. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function emergency($message, array $context = array()) - { - return; - } - - /** - * Action must be taken immediately. - * - * Example: Entire website down, database unavailable, etc. This should - * trigger the SMS alerts and wake you up. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function alert($message, array $context = array()) - { - return; - } - - /** - * Critical conditions. - * - * Example: Application component unavailable, unexpected exception. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function critical($message, array $context = array()) - { - return; - } - - /** - * Runtime errors that do not require immediate action but should typically - * be logged and monitored. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function error($message, array $context = array()) - { - return; - } - - /** - * Exceptional occurrences that are not errors. - * - * Example: Use of deprecated APIs, poor use of an API, undesirable things - * that are not necessarily wrong. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function warning($message, array $context = array()) - { - return; - } - - /** - * Normal but significant events. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function notice($message, array $context = array()) - { - return; - } - - /** - * Interesting events. - * - * Example: User logs in, SQL logs. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function info($message, array $context = array()) - { - return; - } - - /** - * Detailed debug information. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function debug($message, array $context = array()) - { - return; - } - - /** - * Logs with an arbitrary level. - * - * @param mixed $level - * @param string $message - * @param array $context - * - * @return void - */ - public function log($level, $message, array $context = array()) - { - return; - } -} diff --git a/src/Util/Logger/WorkerLogger.php b/src/Util/Logger/WorkerLogger.php deleted file mode 100644 index fdda6c9b53..0000000000 --- a/src/Util/Logger/WorkerLogger.php +++ /dev/null @@ -1,228 +0,0 @@ -. - * - */ - -namespace Friendica\Util\Logger; - -use Friendica\Util\Strings; -use Psr\Log\LoggerInterface; - -/** - * A Logger for specific worker tasks, which adds an additional woker-id to it. - * Uses the decorator pattern (https://en.wikipedia.org/wiki/Decorator_pattern) - */ -class WorkerLogger implements LoggerInterface -{ - /** - * @var LoggerInterface The original Logger instance - */ - private $logger; - - /** - * @var string the current worker ID - */ - private $workerId; - - /** - * @var string The called function name - */ - private $functionName; - - /** - * @param LoggerInterface $logger The logger for worker entries - * @param string $functionName The current function name of the worker - * @param int $idLength The length of the generated worker ID - */ - public function __construct(LoggerInterface $logger, $functionName = '', $idLength = 7) - { - $this->logger = $logger; - $this->functionName = $functionName; - $this->workerId = Strings::getRandomHex($idLength); - } - - /** - * Sets the function name for additional logging - * - * @param string $functionName - */ - public function setFunctionName(string $functionName) - { - $this->functionName = $functionName; - } - - /** - * Adds the worker context for each log entry - * - * @param array $context - */ - private function addContext(array &$context) - { - $context['worker_id'] = $this->workerId; - $context['worker_cmd'] = $this->functionName; - } - - /** - * Returns the worker ID - * - * @return string - */ - public function getWorkerId() - { - return $this->workerId; - } - - /** - * System is unusable. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function emergency($message, array $context = []) - { - $this->addContext($context); - $this->logger->emergency($message, $context); - } - - /** - * Action must be taken immediately. - * - * Example: Entire website down, database unavailable, etc. This should - * trigger the SMS alerts and wake you up. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function alert($message, array $context = []) - { - $this->addContext($context); - $this->logger->alert($message, $context); - } - - /** - * Critical conditions. - * - * Example: Application component unavailable, unexpected exception. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function critical($message, array $context = []) - { - $this->addContext($context); - $this->logger->critical($message, $context); - } - - /** - * Runtime errors that do not require immediate action but should typically - * be logged and monitored. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function error($message, array $context = []) - { - $this->addContext($context); - $this->logger->error($message, $context); - } - - /** - * Exceptional occurrences that are not errors. - * - * Example: Use of deprecated APIs, poor use of an API, undesirable things - * that are not necessarily wrong. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function warning($message, array $context = []) - { - $this->addContext($context); - $this->logger->warning($message, $context); - } - - /** - * Normal but significant events. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function notice($message, array $context = []) - { - $this->addContext($context); - $this->logger->notice($message, $context); - } - - /** - * Interesting events. - * - * Example: User logs in, SQL logs. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function info($message, array $context = []) - { - $this->addContext($context); - $this->logger->info($message, $context); - } - - /** - * Detailed debug information. - * - * @param string $message - * @param array $context - * - * @return void - */ - public function debug($message, array $context = []) - { - $this->addContext($context); - $this->logger->debug($message, $context); - } - - /** - * Logs with an arbitrary level. - * - * @param mixed $level - * @param string $message - * @param array $context - * - * @return void - */ - public function log($level, $message, array $context = []) - { - $this->addContext($context); - $this->logger->log($level, $message, $context); - } -} diff --git a/static/dependencies.config.php b/static/dependencies.config.php index bbf8c5599c..042949e60e 100644 --- a/static/dependencies.config.php +++ b/static/dependencies.config.php @@ -141,7 +141,7 @@ return [ * and is automatically passed as an argument with the same name */ LoggerInterface::class => [ - 'instanceOf' => Factory\LoggerFactory::class, + 'instanceOf' => \Friendica\Core\Logger\Factory\Logger::class, 'constructParams' => [ 'index', ], @@ -150,7 +150,7 @@ return [ ], ], '$devLogger' => [ - 'instanceOf' => Factory\LoggerFactory::class, + 'instanceOf' => \Friendica\Core\Logger\Factory\Logger::class, 'constructParams' => [ 'dev', ], diff --git a/tests/src/Console/AutomaticInstallationConsoleTest.php b/tests/src/Console/AutomaticInstallationConsoleTest.php index 760a732d67..3f4f8c03c9 100644 --- a/tests/src/Console/AutomaticInstallationConsoleTest.php +++ b/tests/src/Console/AutomaticInstallationConsoleTest.php @@ -32,7 +32,7 @@ use Friendica\Database\Database; use Friendica\DI; use Friendica\Test\Util\RendererMockTrait; use Friendica\Test\Util\VFSTrait; -use Friendica\Util\Logger\VoidLogger; +use Friendica\Core\Logger\Type\VoidLogger; use Mockery; use Mockery\MockInterface; use org\bovigo\vfs\vfsStream; diff --git a/tests/src/Contact/FriendSuggest/Factory/FriendSuggestTest.php b/tests/src/Contact/FriendSuggest/Factory/FriendSuggestTest.php index afa0711ae0..9691073168 100644 --- a/tests/src/Contact/FriendSuggest/Factory/FriendSuggestTest.php +++ b/tests/src/Contact/FriendSuggest/Factory/FriendSuggestTest.php @@ -5,7 +5,7 @@ namespace Friendica\Test\src\Contact\FriendSuggest\Factory; use Friendica\Contact\FriendSuggest\Factory\FriendSuggest; use Friendica\Contact\FriendSuggest\Entity; use Friendica\Test\MockedTest; -use Friendica\Util\Logger\VoidLogger; +use Friendica\Core\Logger\Type\VoidLogger; class FriendSuggestTest extends MockedTest { diff --git a/tests/src/Core/Logger/AbstractLoggerTest.php b/tests/src/Core/Logger/AbstractLoggerTest.php new file mode 100644 index 0000000000..f1a0553102 --- /dev/null +++ b/tests/src/Core/Logger/AbstractLoggerTest.php @@ -0,0 +1,192 @@ +. + * + */ + +namespace Friendica\Test\src\Core\Logger; + +use Friendica\Test\MockedTest; +use Friendica\Util\Introspection; +use Mockery\MockInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; + +abstract class AbstractLoggerTest extends MockedTest +{ + use LoggerDataTrait; + + const LOGLINE = '/.* \[.*\]: .* \{.*\"file\":\".*\".*,.*\"line\":\d*,.*\"function\":\".*\".*,.*\"uid\":\".*\".*}/'; + + const FILE = 'test'; + const LINE = 666; + const FUNC = 'myfunction'; + + /** + * @var Introspection|MockInterface + */ + protected $introspection; + + /** + * Returns the content of the current logger instance + * + * @return string + */ + abstract protected function getContent(); + + /** + * Returns the current logger instance + * + * @param string $level the default loglevel + * + * @return LoggerInterface + */ + abstract protected function getInstance($level = LogLevel::DEBUG); + + protected function setUp(): void + { + parent::setUp(); + + $this->introspection = \Mockery::mock(Introspection::class); + $this->introspection->shouldReceive('getRecord')->andReturn([ + 'file' => self::FILE, + 'line' => self::LINE, + 'function' => self::FUNC + ]); + } + + public function assertLogline($string) + { + self::assertRegExp(self::LOGLINE, $string); + } + + public function assertLoglineNums($assertNum, $string) + { + self::assertEquals($assertNum, preg_match_all(self::LOGLINE, $string)); + } + + /** + * Test if the logger works correctly + */ + public function testNormal() + { + $logger = $this->getInstance(); + $logger->emergency('working!'); + $logger->alert('working too!'); + $logger->debug('and now?'); + $logger->notice('message', ['an' => 'context']); + + $text = $this->getContent(); + self::assertLogline($text); + self::assertLoglineNums(4, $text); + } + + /** + * Test if a log entry is correctly interpolated + */ + public function testPsrInterpolate() + { + $logger = $this->getInstance(); + + $logger->emergency('A {psr} test', ['psr' => 'working']); + $logger->alert('An {array} test', ['array' => ['it', 'is', 'working']]); + $text = $this->getContent(); + self::assertStringContainsString('A working test', $text); + self::assertStringContainsString('An ["it","is","working"] test', $text); + } + + /** + * Test if a log entry contains all necessary information + */ + public function testContainsInformation() + { + $logger = $this->getInstance(); + $logger->emergency('A test'); + + $text = $this->getContent(); + self::assertStringContainsString('"file":"' . self::FILE . '"', $text); + self::assertStringContainsString('"line":' . self::LINE, $text); + self::assertStringContainsString('"function":"' . self::FUNC . '"', $text); + } + + /** + * Test if the minimum level is working + */ + public function testMinimumLevel() + { + $logger = $this->getInstance(LogLevel::NOTICE); + + $logger->emergency('working'); + $logger->alert('working'); + $logger->error('working'); + $logger->warning('working'); + $logger->notice('working'); + $logger->info('not working'); + $logger->debug('not working'); + + $text = $this->getContent(); + + self::assertLoglineNums(5, $text); + } + + /** + * Test with different logging data + * @dataProvider dataTests + */ + public function testDifferentTypes($function, $message, array $context) + { + $logger = $this->getInstance(); + $logger->$function($message, $context); + + $text = $this->getContent(); + + self::assertLogline($text); + + self::assertStringContainsString(@json_encode($context), $text); + } + + /** + * Test a message with an exception + */ + public function testExceptionHandling() + { + $e = new \Exception("Test String", 123); + $eFollowUp = new \Exception("FollowUp", 456, $e); + + $assertion = $eFollowUp->__toString(); + + $logger = $this->getInstance(); + $logger->alert('test', ['e' => $eFollowUp]); + $text = $this->getContent(); + + self::assertLogline($text); + + self::assertStringContainsString(@json_encode($assertion), $this->getContent()); + } + + public function testNoObjectHandling() + { + $logger = $this->getInstance(); + $logger->alert('test', ['e' => ['test' => 'test']]); + $text = $this->getContent(); + + self::assertLogline($text); + + self::assertStringContainsString('test', $this->getContent()); + } +} diff --git a/tests/src/Core/Logger/LoggerDataTrait.php b/tests/src/Core/Logger/LoggerDataTrait.php new file mode 100644 index 0000000000..0d6b004cfe --- /dev/null +++ b/tests/src/Core/Logger/LoggerDataTrait.php @@ -0,0 +1,71 @@ +. + * + */ + +namespace Friendica\Test\src\Core\Logger; + +trait LoggerDataTrait +{ + public function dataTests() + { + return [ + 'emergency' => [ + 'function' => 'emergency', + 'message' => 'test', + 'context' => ['a' => 'context'], + ], + 'alert' => [ + 'function' => 'alert', + 'message' => 'test {test}', + 'context' => ['a' => 'context', 2 => 'so', 'test' => 'works'], + ], + 'critical' => [ + 'function' => 'critical', + 'message' => 'test crit 2345', + 'context' => ['a' => 'context', 'wit' => ['more', 'array']], + ], + 'error' => [ + 'function' => 'error', + 'message' => 2.554, + 'context' => [], + ], + 'warning' => [ + 'function' => 'warning', + 'message' => 'test warn', + 'context' => ['a' => 'context'], + ], + 'notice' => [ + 'function' => 'notice', + 'message' => 2346, + 'context' => ['a' => 'context'], + ], + 'info' => [ + 'function' => 'info', + 'message' => null, + 'context' => ['a' => 'context'], + ], + 'debug' => [ + 'function' => 'debug', + 'message' => true, + 'context' => ['a' => false], + ], + ]; + } +} diff --git a/tests/src/Core/Logger/ProfilerLoggerTest.php b/tests/src/Core/Logger/ProfilerLoggerTest.php new file mode 100644 index 0000000000..3b8e7711f8 --- /dev/null +++ b/tests/src/Core/Logger/ProfilerLoggerTest.php @@ -0,0 +1,83 @@ +. + * + */ + +namespace Friendica\Test\src\Core\Logger; + +use Friendica\Test\MockedTest; +use Friendica\Core\Logger\Type\ProfilerLogger; +use Friendica\Util\Profiler; +use Mockery\MockInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; + +class ProfilerLoggerTest extends MockedTest +{ + use LoggerDataTrait; + + /** + * @var LoggerInterface|MockInterface + */ + private $logger; + /** + * @var Profiler|MockInterface + */ + private $profiler; + + protected function setUp(): void + { + parent::setUp(); + + $this->logger = \Mockery::mock(LoggerInterface::class); + $this->profiler = \Mockery::mock(Profiler::class); + } + + /** + * Test if the profiler is profiling data + * @dataProvider dataTests + * @doesNotPerformAssertions + */ + public function testProfiling($function, $message, array $context) + { + $logger = new ProfilerLogger($this->logger, $this->profiler); + + $this->logger->shouldReceive($function)->with($message, $context)->once(); + $this->profiler->shouldReceive('startRecording')->with('file')->once(); + $this->profiler->shouldReceive('stopRecording'); + $this->profiler->shouldReceive('saveTimestamp'); + $logger->$function($message, $context); + } + + /** + * Test the log() function + * @doesNotPerformAssertions + */ + public function testProfilingLog() + { + $logger = new ProfilerLogger($this->logger, $this->profiler); + + $this->logger->shouldReceive('log')->with(LogLevel::WARNING, 'test', ['a' => 'context'])->once(); + $this->profiler->shouldReceive('startRecording')->with('file')->once(); + $this->profiler->shouldReceive('stopRecording'); + $this->profiler->shouldReceive('saveTimestamp'); + + $logger->log(LogLevel::WARNING, 'test', ['a' => 'context']); + } +} diff --git a/tests/src/Core/Logger/StreamLoggerTest.php b/tests/src/Core/Logger/StreamLoggerTest.php new file mode 100644 index 0000000000..65ef76ea3d --- /dev/null +++ b/tests/src/Core/Logger/StreamLoggerTest.php @@ -0,0 +1,213 @@ +. + * + */ + +namespace Friendica\Test\src\Core\Logger; + +use Friendica\Core\Logger\Exception\LoggerArgumentException; +use Friendica\Core\Logger\Exception\LoggerException; +use Friendica\Util\FileSystem; +use Friendica\Test\Util\VFSTrait; +use Friendica\Core\Logger\Type\StreamLogger; +use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamFile; +use Psr\Log\LogLevel; + +class StreamLoggerTest extends AbstractLoggerTest +{ + use VFSTrait; + + /** + * @var vfsStreamFile + */ + private $logfile; + + /** + * @var Filesystem + */ + private $fileSystem; + + protected function setUp(): void + { + parent::setUp(); + + $this->setUpVfsDir(); + + $this->fileSystem = new FileSystem(); + } + + /** + * {@@inheritdoc} + */ + protected function getInstance($level = LogLevel::DEBUG) + { + $this->logfile = vfsStream::newFile('friendica.log') + ->at($this->root); + + $logger = new StreamLogger('test', $this->logfile->url(), $this->introspection, $this->fileSystem, $level); + + return $logger; + } + + /** + * {@inheritdoc} + */ + protected function getContent() + { + return $this->logfile->getContent(); + } + + /** + * Test if a stream is working + */ + public function testStream() + { + $logfile = vfsStream::newFile('friendica.log') + ->at($this->root); + + $filehandler = fopen($logfile->url(), 'ab'); + + $logger = new \Friendica\Core\Logger\Type\StreamLogger('test', $filehandler, $this->introspection, $this->fileSystem); + $logger->emergency('working'); + + $text = $logfile->getContent(); + + self::assertLogline($text); + } + + /** + * Test if the close statement is working + */ + public function testClose() + { + $logfile = vfsStream::newFile('friendica.log') + ->at($this->root); + + $logger = new StreamLogger('test', $logfile->url(), $this->introspection, $this->fileSystem); + $logger->emergency('working'); + $logger->close(); + // close doesn't affect + $logger->emergency('working too'); + + $text = $logfile->getContent(); + + self::assertLoglineNums(2, $text); + } + + /** + * Test when a file isn't set + */ + public function testNoUrl() + { + $this->expectException(LoggerArgumentException::class); + $this->expectExceptionMessage("Missing stream URL."); + + $logger = new StreamLogger('test', '', $this->introspection, $this->fileSystem); + + $logger->emergency('not working'); + } + + /** + * Test when a file cannot be opened + */ + public function testWrongUrl() + { + $this->expectException(LoggerException::class); + $this->expectExceptionMessage("Cannot create stream."); + + $logfile = vfsStream::newFile('friendica.log') + ->at($this->root)->chmod(0); + + $logger = new StreamLogger('test', $logfile->url(), $this->introspection, $this->fileSystem); + + $logger->emergency('not working'); + } + + /** + * Test when the directory cannot get created + */ + public function testWrongDir() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageMatches("/Directory .* cannot get created: .* /"); + + static::markTestIncomplete('We need a platform independent way to set directory to readonly'); + + $logger = new StreamLogger('test', '/$%/wrong/directory/file.txt', $this->introspection, $this->fileSystem); + + $logger->emergency('not working'); + } + + /** + * Test when the minimum level is not valid + */ + public function testWrongMinimumLevel() + { + $this->expectException(LoggerArgumentException::class); + $this->expectExceptionMessageMatches("/The level \".*\" is not valid./"); + + $logger = new StreamLogger('test', 'file.text', $this->introspection, $this->fileSystem, 'NOPE'); + } + + /** + * Test when the minimum level is not valid + */ + public function testWrongLogLevel() + { + $this->expectException(LoggerArgumentException::class); + $this->expectExceptionMessageMatches("/The level \".*\" is not valid./"); + + $logfile = vfsStream::newFile('friendica.log') + ->at($this->root); + + $logger = new StreamLogger('test', $logfile->url(), $this->introspection, $this->fileSystem); + + $logger->log('NOPE', 'a test'); + } + + /** + * Test when the file is null + */ + public function testWrongFile() + { + $this->expectException(LoggerArgumentException::class); + $this->expectExceptionMessage("A stream must either be a resource or a string."); + + $logger = new StreamLogger('test', null, $this->introspection, $this->fileSystem); + } + + /** + * Test a relative path + * @doesNotPerformAssertions + */ + public function testRealPath() + { + static::markTestSkipped('vfsStream isn\'t compatible with chdir, so not testable.'); + + $logfile = vfsStream::newFile('friendica.log') + ->at($this->root); + + chdir($this->root->getChild('logs')->url()); + + $logger = new StreamLogger('test', '../friendica.log' , $this->introspection, $this->fileSystem); + + $logger->info('Test'); + } +} diff --git a/tests/src/Core/Logger/SyslogLoggerTest.php b/tests/src/Core/Logger/SyslogLoggerTest.php new file mode 100644 index 0000000000..8ba2ebc084 --- /dev/null +++ b/tests/src/Core/Logger/SyslogLoggerTest.php @@ -0,0 +1,98 @@ +. + * + */ + +namespace Friendica\Test\src\Core\Logger; + +use Friendica\Core\Logger\Exception\LoggerArgumentException; +use Friendica\Core\Logger\Exception\LoggerException; +use Friendica\Core\Logger\Type\SyslogLogger; +use Psr\Log\LogLevel; + +class SyslogLoggerTest extends AbstractLoggerTest +{ + /** + * @var SyslogLoggerWrapper + */ + private $logger; + + protected function setUp(): void + { + parent::setUp(); + + $this->introspection->shouldReceive('addClasses')->with([SyslogLogger::class]); + } + + /** + * {@inheritdoc} + */ + protected function getContent() + { + return $this->logger->getContent(); + } + + /** + * {@inheritdoc} + */ + protected function getInstance($level = LogLevel::DEBUG) + { + $this->logger = new SyslogLoggerWrapper('test', $this->introspection, $level); + + return $this->logger; + } + + + /** + * Test when the minimum level is not valid + */ + public function testWrongMinimumLevel() + { + $this->expectException(LoggerArgumentException::class); + $this->expectExceptionMessageMatches("/The level \".*\" is not valid./"); + + $logger = new SyslogLoggerWrapper('test', $this->introspection, 'NOPE'); + } + + /** + * Test when the minimum level is not valid + */ + public function testWrongLogLevel() + { + $this->expectException(LoggerArgumentException::class); + $this->expectExceptionMessageMatches("/The level \".*\" is not valid./"); + + $logger = new SyslogLoggerWrapper('test', $this->introspection); + + $logger->log('NOPE', 'a test'); + } + + /** + * Test the close() method + * @doesNotPerformAssertions + */ + public function testClose() + { + $logger = new SyslogLoggerWrapper('test', $this->introspection); + $logger->emergency('test'); + $logger->close(); + // Reopened itself + $logger->emergency('test'); + } +} diff --git a/tests/src/Core/Logger/SyslogLoggerWrapper.php b/tests/src/Core/Logger/SyslogLoggerWrapper.php new file mode 100644 index 0000000000..05dcbd6bc5 --- /dev/null +++ b/tests/src/Core/Logger/SyslogLoggerWrapper.php @@ -0,0 +1,60 @@ +. + * + */ + +namespace Friendica\Test\src\Core\Logger; + +use Friendica\Core\Logger\Type\SyslogLogger; +use Friendica\Util\Introspection; +use Psr\Log\LogLevel; + +/** + * Wraps the SyslogLogger for replacing the syslog call with a string field. + */ +class SyslogLoggerWrapper extends SyslogLogger +{ + private $content; + + public function __construct($channel, Introspection $introspection, $level = LogLevel::NOTICE, $logOpts = LOG_PID, $logFacility = LOG_USER) + { + parent::__construct($channel, $introspection, $level, $logOpts, $logFacility); + + $this->content = ''; + } + + /** + * Gets the content from the wrapped Syslog + * + * @return string + */ + public function getContent() + { + return $this->content; + } + + /** + * {@inheritdoc} + * @noinspection PhpMissingParentCallCommonInspection + */ + protected function syslogWrapper(int $level, string $entry) + { + $this->content .= $entry . PHP_EOL; + } +} diff --git a/tests/src/Core/Logger/VoidLoggerTest.php b/tests/src/Core/Logger/VoidLoggerTest.php new file mode 100644 index 0000000000..a2134ce711 --- /dev/null +++ b/tests/src/Core/Logger/VoidLoggerTest.php @@ -0,0 +1,52 @@ +. + * + */ + +namespace Friendica\Test\src\Core\Logger; + +use Friendica\Test\MockedTest; +use Friendica\Core\Logger\Type\VoidLogger; +use Psr\Log\LogLevel; + +class VoidLoggerTest extends MockedTest +{ + use LoggerDataTrait; + + /** + * Test if the profiler is profiling data + * @dataProvider dataTests + * @doesNotPerformAssertions + */ + public function testNormal($function, $message, array $context) + { + $logger = new VoidLogger(); + $logger->$function($message, $context); + } + + /** + * Test the log() function + * @doesNotPerformAssertions + */ + public function testProfilingLog() + { + $logger = new VoidLogger(); + $logger->log(LogLevel::WARNING, 'test', ['a' => 'context']); + } +} diff --git a/tests/src/Core/Logger/WorkerLoggerTest.php b/tests/src/Core/Logger/WorkerLoggerTest.php new file mode 100644 index 0000000000..0f751f2e4d --- /dev/null +++ b/tests/src/Core/Logger/WorkerLoggerTest.php @@ -0,0 +1,137 @@ +. + * + */ + +namespace Friendica\Test\src\Core\Logger; + +use Friendica\Core\Logger\Type\WorkerLogger; +use Friendica\Test\MockedTest; +use Psr\Log\LoggerInterface; + +class WorkerLoggerTest extends MockedTest +{ + private function assertUid($uid, $length = 7) + { + self::assertRegExp('/^[a-zA-Z0-9]{' . $length . '}+$/', $uid); + } + + /** + * Test the a id with length zero + * + */ + public function testGetWorkerIdZero() + { + $this->expectException(\Error::class); + + $logger = \Mockery::mock(LoggerInterface::class); + new WorkerLogger($logger, 'test', 0); + } + + /** + * Test the generated Uid + */ + public function testGetWorkerId() + { + $logger = \Mockery::mock(LoggerInterface::class); + for ($i = 1; $i < 14; $i++) { + $workLogger = new WorkerLogger($logger, 'test', $i); + $uid = $workLogger->getWorkerId(); + self::assertUid($uid, $i); + } + } + + public function dataTest() + { + return [ + 'info' => [ + 'func' => 'info', + 'msg' => 'the alert', + 'context' => [], + ], + 'alert' => [ + 'func' => 'alert', + 'msg' => 'another alert', + 'context' => ['test' => 'it'], + ], + 'critical' => [ + 'func' => 'critical', + 'msg' => 'Critical msg used', + 'context' => ['test' => 'it', 'more' => 0.24545], + ], + 'error' => [ + 'func' => 'error', + 'msg' => 21345623, + 'context' => ['test' => 'it', 'yet' => true], + ], + 'warning' => [ + 'func' => 'warning', + 'msg' => 'another alert' . 123523 . 324.54534 . 'test', + 'context' => ['test' => 'it', 2 => 'nope'], + ], + 'notice' => [ + 'func' => 'notice', + 'msg' => 'Notice' . ' alert' . true . 'with' . '\'strange\'' . 1.24. 'behavior', + 'context' => ['test' => 'it'], + ], + 'debug' => [ + 'func' => 'debug', + 'msg' => 'at last a debug', + 'context' => ['test' => 'it'], + ], + ]; + } + + /** + * Test the WorkerLogger with different log calls + * @dataProvider dataTest + */ + public function testEmergency($func, $msg, $context = []) + { + $logger = \Mockery::mock(LoggerInterface::class); + $workLogger = new WorkerLogger($logger, 'test'); + $testContext = $context; + $testContext['worker_id'] = $workLogger->getWorkerId(); + $testContext['worker_cmd'] = 'test'; + self::assertUid($testContext['worker_id']); + $logger + ->shouldReceive($func) + ->with($msg, $testContext) + ->once(); + $workLogger->$func($msg, $context); + } + + /** + * Test the WorkerLogger with + */ + public function testLog() + { + $logger = \Mockery::mock(LoggerInterface::class); + $workLogger = new WorkerLogger($logger, 'test'); + $context = $testContext = ['test' => 'it']; + $testContext['worker_id'] = $workLogger->getWorkerId(); + $testContext['worker_cmd'] = 'test'; + self::assertUid($testContext['worker_id']); + $logger + ->shouldReceive('log') + ->with('debug', 'a test', $testContext) + ->once(); + $workLogger->log('debug', 'a test', $context); + } +} diff --git a/tests/src/Profile/ProfileField/Entity/ProfileFieldTest.php b/tests/src/Profile/ProfileField/Entity/ProfileFieldTest.php index 275a1d5972..6e054428d8 100644 --- a/tests/src/Profile/ProfileField/Entity/ProfileFieldTest.php +++ b/tests/src/Profile/ProfileField/Entity/ProfileFieldTest.php @@ -11,7 +11,7 @@ use Friendica\Security\PermissionSet\Factory\PermissionSet as PermissionSetFacto use Friendica\Test\MockedTest; use Friendica\Util\ACLFormatter; use Friendica\Util\DateTimeFormat; -use Friendica\Util\Logger\VoidLogger; +use Friendica\Core\Logger\Type\VoidLogger; use Mockery\MockInterface; class ProfileFieldTest extends MockedTest diff --git a/tests/src/Security/TwoFactor/Factory/TrustedBrowserTest.php b/tests/src/Security/TwoFactor/Factory/TrustedBrowserTest.php index 0a093db5a1..e27445d32b 100644 --- a/tests/src/Security/TwoFactor/Factory/TrustedBrowserTest.php +++ b/tests/src/Security/TwoFactor/Factory/TrustedBrowserTest.php @@ -5,7 +5,7 @@ namespace Friendica\Test\src\Security\TwoFactor\Factory; use Friendica\Security\TwoFactor\Factory\TrustedBrowser; use Friendica\Test\MockedTest; use Friendica\Util\DateTimeFormat; -use Friendica\Util\Logger\VoidLogger; +use Friendica\Core\Logger\Type\VoidLogger; use Friendica\Util\Strings; class TrustedBrowserTest extends MockedTest diff --git a/tests/src/Util/Logger/AbstractLoggerTest.php b/tests/src/Util/Logger/AbstractLoggerTest.php deleted file mode 100644 index d008f4dc42..0000000000 --- a/tests/src/Util/Logger/AbstractLoggerTest.php +++ /dev/null @@ -1,192 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Util\Logger; - -use Friendica\Test\MockedTest; -use Friendica\Util\Introspection; -use Mockery\MockInterface; -use Psr\Log\LoggerInterface; -use Psr\Log\LogLevel; - -abstract class AbstractLoggerTest extends MockedTest -{ - use LoggerDataTrait; - - const LOGLINE = '/.* \[.*\]: .* \{.*\"file\":\".*\".*,.*\"line\":\d*,.*\"function\":\".*\".*,.*\"uid\":\".*\".*}/'; - - const FILE = 'test'; - const LINE = 666; - const FUNC = 'myfunction'; - - /** - * @var Introspection|MockInterface - */ - protected $introspection; - - /** - * Returns the content of the current logger instance - * - * @return string - */ - abstract protected function getContent(); - - /** - * Returns the current logger instance - * - * @param string $level the default loglevel - * - * @return LoggerInterface - */ - abstract protected function getInstance($level = LogLevel::DEBUG); - - protected function setUp(): void - { - parent::setUp(); - - $this->introspection = \Mockery::mock(Introspection::class); - $this->introspection->shouldReceive('getRecord')->andReturn([ - 'file' => self::FILE, - 'line' => self::LINE, - 'function' => self::FUNC - ]); - } - - public function assertLogline($string) - { - self::assertRegExp(self::LOGLINE, $string); - } - - public function assertLoglineNums($assertNum, $string) - { - self::assertEquals($assertNum, preg_match_all(self::LOGLINE, $string)); - } - - /** - * Test if the logger works correctly - */ - public function testNormal() - { - $logger = $this->getInstance(); - $logger->emergency('working!'); - $logger->alert('working too!'); - $logger->debug('and now?'); - $logger->notice('message', ['an' => 'context']); - - $text = $this->getContent(); - self::assertLogline($text); - self::assertLoglineNums(4, $text); - } - - /** - * Test if a log entry is correctly interpolated - */ - public function testPsrInterpolate() - { - $logger = $this->getInstance(); - - $logger->emergency('A {psr} test', ['psr' => 'working']); - $logger->alert('An {array} test', ['array' => ['it', 'is', 'working']]); - $text = $this->getContent(); - self::assertStringContainsString('A working test', $text); - self::assertStringContainsString('An ["it","is","working"] test', $text); - } - - /** - * Test if a log entry contains all necessary information - */ - public function testContainsInformation() - { - $logger = $this->getInstance(); - $logger->emergency('A test'); - - $text = $this->getContent(); - self::assertStringContainsString('"file":"' . self::FILE . '"', $text); - self::assertStringContainsString('"line":' . self::LINE, $text); - self::assertStringContainsString('"function":"' . self::FUNC . '"', $text); - } - - /** - * Test if the minimum level is working - */ - public function testMinimumLevel() - { - $logger = $this->getInstance(LogLevel::NOTICE); - - $logger->emergency('working'); - $logger->alert('working'); - $logger->error('working'); - $logger->warning('working'); - $logger->notice('working'); - $logger->info('not working'); - $logger->debug('not working'); - - $text = $this->getContent(); - - self::assertLoglineNums(5, $text); - } - - /** - * Test with different logging data - * @dataProvider dataTests - */ - public function testDifferentTypes($function, $message, array $context) - { - $logger = $this->getInstance(); - $logger->$function($message, $context); - - $text = $this->getContent(); - - self::assertLogline($text); - - self::assertStringContainsString(@json_encode($context), $text); - } - - /** - * Test a message with an exception - */ - public function testExceptionHandling() - { - $e = new \Exception("Test String", 123); - $eFollowUp = new \Exception("FollowUp", 456, $e); - - $assertion = $eFollowUp->__toString(); - - $logger = $this->getInstance(); - $logger->alert('test', ['e' => $eFollowUp]); - $text = $this->getContent(); - - self::assertLogline($text); - - self::assertStringContainsString(@json_encode($assertion), $this->getContent()); - } - - public function testNoObjectHandling() - { - $logger = $this->getInstance(); - $logger->alert('test', ['e' => ['test' => 'test']]); - $text = $this->getContent(); - - self::assertLogline($text); - - self::assertStringContainsString('test', $this->getContent()); - } -} diff --git a/tests/src/Util/Logger/LoggerDataTrait.php b/tests/src/Util/Logger/LoggerDataTrait.php deleted file mode 100644 index 0bebe7d2cb..0000000000 --- a/tests/src/Util/Logger/LoggerDataTrait.php +++ /dev/null @@ -1,71 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Util\Logger; - -trait LoggerDataTrait -{ - public function dataTests() - { - return [ - 'emergency' => [ - 'function' => 'emergency', - 'message' => 'test', - 'context' => ['a' => 'context'], - ], - 'alert' => [ - 'function' => 'alert', - 'message' => 'test {test}', - 'context' => ['a' => 'context', 2 => 'so', 'test' => 'works'], - ], - 'critical' => [ - 'function' => 'critical', - 'message' => 'test crit 2345', - 'context' => ['a' => 'context', 'wit' => ['more', 'array']], - ], - 'error' => [ - 'function' => 'error', - 'message' => 2.554, - 'context' => [], - ], - 'warning' => [ - 'function' => 'warning', - 'message' => 'test warn', - 'context' => ['a' => 'context'], - ], - 'notice' => [ - 'function' => 'notice', - 'message' => 2346, - 'context' => ['a' => 'context'], - ], - 'info' => [ - 'function' => 'info', - 'message' => null, - 'context' => ['a' => 'context'], - ], - 'debug' => [ - 'function' => 'debug', - 'message' => true, - 'context' => ['a' => false], - ], - ]; - } -} diff --git a/tests/src/Util/Logger/ProfilerLoggerTest.php b/tests/src/Util/Logger/ProfilerLoggerTest.php deleted file mode 100644 index 7b62446142..0000000000 --- a/tests/src/Util/Logger/ProfilerLoggerTest.php +++ /dev/null @@ -1,83 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Util\Logger; - -use Friendica\Test\MockedTest; -use Friendica\Util\Logger\ProfilerLogger; -use Friendica\Util\Profiler; -use Mockery\MockInterface; -use Psr\Log\LoggerInterface; -use Psr\Log\LogLevel; - -class ProfilerLoggerTest extends MockedTest -{ - use LoggerDataTrait; - - /** - * @var LoggerInterface|MockInterface - */ - private $logger; - /** - * @var Profiler|MockInterface - */ - private $profiler; - - protected function setUp(): void - { - parent::setUp(); - - $this->logger = \Mockery::mock(LoggerInterface::class); - $this->profiler = \Mockery::mock(Profiler::class); - } - - /** - * Test if the profiler is profiling data - * @dataProvider dataTests - * @doesNotPerformAssertions - */ - public function testProfiling($function, $message, array $context) - { - $logger = new ProfilerLogger($this->logger, $this->profiler); - - $this->logger->shouldReceive($function)->with($message, $context)->once(); - $this->profiler->shouldReceive('startRecording')->with('file')->once(); - $this->profiler->shouldReceive('stopRecording'); - $this->profiler->shouldReceive('saveTimestamp'); - $logger->$function($message, $context); - } - - /** - * Test the log() function - * @doesNotPerformAssertions - */ - public function testProfilingLog() - { - $logger = new ProfilerLogger($this->logger, $this->profiler); - - $this->logger->shouldReceive('log')->with(LogLevel::WARNING, 'test', ['a' => 'context'])->once(); - $this->profiler->shouldReceive('startRecording')->with('file')->once(); - $this->profiler->shouldReceive('stopRecording'); - $this->profiler->shouldReceive('saveTimestamp'); - - $logger->log(LogLevel::WARNING, 'test', ['a' => 'context']); - } -} diff --git a/tests/src/Util/Logger/StreamLoggerTest.php b/tests/src/Util/Logger/StreamLoggerTest.php deleted file mode 100644 index 8599e08f45..0000000000 --- a/tests/src/Util/Logger/StreamLoggerTest.php +++ /dev/null @@ -1,211 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Util\Logger; - -use Friendica\Util\FileSystem; -use Friendica\Test\Util\VFSTrait; -use Friendica\Util\Logger\StreamLogger; -use org\bovigo\vfs\vfsStream; -use org\bovigo\vfs\vfsStreamFile; -use Psr\Log\LogLevel; - -class StreamLoggerTest extends AbstractLoggerTest -{ - use VFSTrait; - - /** - * @var vfsStreamFile - */ - private $logfile; - - /** - * @var Filesystem - */ - private $fileSystem; - - protected function setUp(): void - { - parent::setUp(); - - $this->setUpVfsDir(); - - $this->fileSystem = new FileSystem(); - } - - /** - * {@@inheritdoc} - */ - protected function getInstance($level = LogLevel::DEBUG) - { - $this->logfile = vfsStream::newFile('friendica.log') - ->at($this->root); - - $logger = new StreamLogger('test', $this->logfile->url(), $this->introspection, $this->fileSystem, $level); - - return $logger; - } - - /** - * {@inheritdoc} - */ - protected function getContent() - { - return $this->logfile->getContent(); - } - - /** - * Test if a stream is working - */ - public function testStream() - { - $logfile = vfsStream::newFile('friendica.log') - ->at($this->root); - - $filehandler = fopen($logfile->url(), 'ab'); - - $logger = new StreamLogger('test', $filehandler, $this->introspection, $this->fileSystem); - $logger->emergency('working'); - - $text = $logfile->getContent(); - - self::assertLogline($text); - } - - /** - * Test if the close statement is working - */ - public function testClose() - { - $logfile = vfsStream::newFile('friendica.log') - ->at($this->root); - - $logger = new StreamLogger('test', $logfile->url(), $this->introspection, $this->fileSystem); - $logger->emergency('working'); - $logger->close(); - // close doesn't affect - $logger->emergency('working too'); - - $text = $logfile->getContent(); - - self::assertLoglineNums(2, $text); - } - - /** - * Test when a file isn't set - */ - public function testNoUrl() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage("Missing stream URL."); - - $logger = new StreamLogger('test', '', $this->introspection, $this->fileSystem); - - $logger->emergency('not working'); - } - - /** - * Test when a file cannot be opened - */ - public function testWrongUrl() - { - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessageMatches("/The stream or file .* could not be opened: .* /"); - - $logfile = vfsStream::newFile('friendica.log') - ->at($this->root)->chmod(0); - - $logger = new StreamLogger('test', $logfile->url(), $this->introspection, $this->fileSystem); - - $logger->emergency('not working'); - } - - /** - * Test when the directory cannot get created - */ - public function testWrongDir() - { - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessageMatches("/Directory .* cannot get created: .* /"); - - static::markTestIncomplete('We need a platform independent way to set directory to readonly'); - - $logger = new StreamLogger('test', '/$%/wrong/directory/file.txt', $this->introspection, $this->fileSystem); - - $logger->emergency('not working'); - } - - /** - * Test when the minimum level is not valid - */ - public function testWrongMinimumLevel() - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches("/The level \".*\" is not valid./"); - - $logger = new StreamLogger('test', 'file.text', $this->introspection, $this->fileSystem, 'NOPE'); - } - - /** - * Test when the minimum level is not valid - */ - public function testWrongLogLevel() - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches("/The level \".*\" is not valid./"); - - $logfile = vfsStream::newFile('friendica.log') - ->at($this->root); - - $logger = new StreamLogger('test', $logfile->url(), $this->introspection, $this->fileSystem); - - $logger->log('NOPE', 'a test'); - } - - /** - * Test when the file is null - */ - public function testWrongFile() - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage("A stream must either be a resource or a string."); - - $logger = new StreamLogger('test', null, $this->introspection, $this->fileSystem); - } - - /** - * Test a relative path - * @doesNotPerformAssertions - */ - public function testRealPath() - { - static::markTestSkipped('vfsStream isn\'t compatible with chdir, so not testable.'); - - $logfile = vfsStream::newFile('friendica.log') - ->at($this->root); - - chdir($this->root->getChild('logs')->url()); - - $logger = new StreamLogger('test', '../friendica.log' , $this->introspection, $this->fileSystem); - - $logger->info('Test'); - } -} diff --git a/tests/src/Util/Logger/SyslogLoggerTest.php b/tests/src/Util/Logger/SyslogLoggerTest.php deleted file mode 100644 index e93e43dd5c..0000000000 --- a/tests/src/Util/Logger/SyslogLoggerTest.php +++ /dev/null @@ -1,113 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Util\Logger; - -use Friendica\Util\Logger\SyslogLogger; -use Psr\Log\LogLevel; - -class SyslogLoggerTest extends AbstractLoggerTest -{ - /** - * @var SyslogLoggerWrapper - */ - private $logger; - - protected function setUp(): void - { - parent::setUp(); - - $this->introspection->shouldReceive('addClasses')->with([SyslogLogger::class]); - } - - /** - * {@inheritdoc} - */ - protected function getContent() - { - return $this->logger->getContent(); - } - - /** - * {@inheritdoc} - */ - protected function getInstance($level = LogLevel::DEBUG) - { - $this->logger = new SyslogLoggerWrapper('test', $this->introspection, $level); - - return $this->logger; - } - - - /** - * Test when the minimum level is not valid - */ - public function testWrongMinimumLevel() - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches("/The level \".*\" is not valid./"); - - $logger = new SyslogLoggerWrapper('test', $this->introspection, 'NOPE'); - } - - /** - * Test when the minimum level is not valid - */ - public function testWrongLogLevel() - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessageMatches("/The level \".*\" is not valid./"); - - $logger = new SyslogLoggerWrapper('test', $this->introspection); - - $logger->log('NOPE', 'a test'); - } - - /** - * Test when the logfacility is wrong (string) - */ - public function testServerException() - { - if (PHP_MAJOR_VERSION < 8) { - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessageMatches("/Can\'t open syslog for ident \".*\" and facility \".*\": .* /"); - } else { - $this->expectException(\TypeError::class); - $this->expectExceptionMessage("openlog(): Argument #3 (\$facility) must be of type int, string given"); - } - - $logger = new SyslogLoggerWrapper('test', $this->introspection, LogLevel::DEBUG, null, 'a string'); - $logger->emergency('not working'); - } - - /** - * Test the close() method - * @doesNotPerformAssertions - */ - public function testClose() - { - $logger = new SyslogLoggerWrapper('test', $this->introspection); - $logger->emergency('test'); - $logger->close(); - // Reopened itself - $logger->emergency('test'); - } -} diff --git a/tests/src/Util/Logger/SyslogLoggerWrapper.php b/tests/src/Util/Logger/SyslogLoggerWrapper.php deleted file mode 100644 index 710899cc2b..0000000000 --- a/tests/src/Util/Logger/SyslogLoggerWrapper.php +++ /dev/null @@ -1,60 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Util\Logger; - -use Friendica\Util\Introspection; -use Friendica\Util\Logger\SyslogLogger; -use Psr\Log\LogLevel; - -/** - * Wraps the SyslogLogger for replacing the syslog call with a string field. - */ -class SyslogLoggerWrapper extends SyslogLogger -{ - private $content; - - public function __construct($channel, Introspection $introspection, $level = LogLevel::NOTICE, $logOpts = LOG_PID, $logFacility = LOG_USER) - { - parent::__construct($channel, $introspection, $level, $logOpts, $logFacility); - - $this->content = ''; - } - - /** - * Gets the content from the wrapped Syslog - * - * @return string - */ - public function getContent() - { - return $this->content; - } - - /** - * {@inheritdoc} - * @noinspection PhpMissingParentCallCommonInspection - */ - protected function syslogWrapper($level, $entry) - { - $this->content .= $entry . PHP_EOL; - } -} diff --git a/tests/src/Util/Logger/VoidLoggerTest.php b/tests/src/Util/Logger/VoidLoggerTest.php deleted file mode 100644 index 75b2d1a8b8..0000000000 --- a/tests/src/Util/Logger/VoidLoggerTest.php +++ /dev/null @@ -1,52 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Util\Logger; - -use Friendica\Test\MockedTest; -use Friendica\Util\Logger\VoidLogger; -use Psr\Log\LogLevel; - -class VoidLoggerTest extends MockedTest -{ - use LoggerDataTrait; - - /** - * Test if the profiler is profiling data - * @dataProvider dataTests - * @doesNotPerformAssertions - */ - public function testNormal($function, $message, array $context) - { - $logger = new VoidLogger(); - $logger->$function($message, $context); - } - - /** - * Test the log() function - * @doesNotPerformAssertions - */ - public function testProfilingLog() - { - $logger = new VoidLogger(); - $logger->log(LogLevel::WARNING, 'test', ['a' => 'context']); - } -} diff --git a/tests/src/Util/Logger/WorkerLoggerTest.php b/tests/src/Util/Logger/WorkerLoggerTest.php deleted file mode 100644 index 06fae3c381..0000000000 --- a/tests/src/Util/Logger/WorkerLoggerTest.php +++ /dev/null @@ -1,137 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Util\Logger; - -use Friendica\Test\MockedTest; -use Friendica\Util\Logger\WorkerLogger; -use Psr\Log\LoggerInterface; - -class WorkerLoggerTest extends MockedTest -{ - private function assertUid($uid, $length = 7) - { - self::assertRegExp('/^[a-zA-Z0-9]{' . $length . '}+$/', $uid); - } - - /** - * Test the a id with length zero - * - */ - public function testGetWorkerIdZero() - { - $this->expectException(\Error::class); - - $logger = \Mockery::mock(LoggerInterface::class); - new WorkerLogger($logger, 'test', 0); - } - - /** - * Test the generated Uid - */ - public function testGetWorkerId() - { - $logger = \Mockery::mock(LoggerInterface::class); - for ($i = 1; $i < 14; $i++) { - $workLogger = new WorkerLogger($logger, 'test', $i); - $uid = $workLogger->getWorkerId(); - self::assertUid($uid, $i); - } - } - - public function dataTest() - { - return [ - 'info' => [ - 'func' => 'info', - 'msg' => 'the alert', - 'context' => [], - ], - 'alert' => [ - 'func' => 'alert', - 'msg' => 'another alert', - 'context' => ['test' => 'it'], - ], - 'critical' => [ - 'func' => 'critical', - 'msg' => 'Critical msg used', - 'context' => ['test' => 'it', 'more' => 0.24545], - ], - 'error' => [ - 'func' => 'error', - 'msg' => 21345623, - 'context' => ['test' => 'it', 'yet' => true], - ], - 'warning' => [ - 'func' => 'warning', - 'msg' => 'another alert' . 123523 . 324.54534 . 'test', - 'context' => ['test' => 'it', 2 => 'nope'], - ], - 'notice' => [ - 'func' => 'notice', - 'msg' => 'Notice' . ' alert' . true . 'with' . '\'strange\'' . 1.24. 'behavior', - 'context' => ['test' => 'it'], - ], - 'debug' => [ - 'func' => 'debug', - 'msg' => 'at last a debug', - 'context' => ['test' => 'it'], - ], - ]; - } - - /** - * Test the WorkerLogger with different log calls - * @dataProvider dataTest - */ - public function testEmergency($func, $msg, $context = []) - { - $logger = \Mockery::mock(LoggerInterface::class); - $workLogger = new WorkerLogger($logger, 'test'); - $testContext = $context; - $testContext['worker_id'] = $workLogger->getWorkerId(); - $testContext['worker_cmd'] = 'test'; - self::assertUid($testContext['worker_id']); - $logger - ->shouldReceive($func) - ->with($msg, $testContext) - ->once(); - $workLogger->$func($msg, $context); - } - - /** - * Test the WorkerLogger with - */ - public function testLog() - { - $logger = \Mockery::mock(LoggerInterface::class); - $workLogger = new WorkerLogger($logger, 'test'); - $context = $testContext = ['test' => 'it']; - $testContext['worker_id'] = $workLogger->getWorkerId(); - $testContext['worker_cmd'] = 'test'; - self::assertUid($testContext['worker_id']); - $logger - ->shouldReceive('log') - ->with('debug', 'a test', $testContext) - ->once(); - $workLogger->log('debug', 'a test', $context); - } -}