diff --git a/config/defaults.config.php b/config/defaults.config.php index d40d0c68d8..b3f195e692 100644 --- a/config/defaults.config.php +++ b/config/defaults.config.php @@ -209,9 +209,9 @@ return [ // If activated, all hashtags will point to the local server. 'local_tags' => false, - // logger_adapter (String) - // Sets the logging adapter of Friendica globally (monolog, syslog) - 'logger_adapter' => 'monolog', + // logger_config (String) + // Sets the logging adapter of Friendica globally (monolog, syslog, stream) + 'logger_config' => 'stream', // max_batch_queue (Integer) // Maximum number of batched queue items for a single contact before subsequent messages are discarded. diff --git a/src/Core/Logger.php b/src/Core/Logger.php index 3cb22e1e47..fc2dde1dfe 100644 --- a/src/Core/Logger.php +++ b/src/Core/Logger.php @@ -4,14 +4,13 @@ */ namespace Friendica\Core; -use Friendica\BaseObject; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; /** * @brief Logger functions */ -class Logger extends BaseObject +class Logger { /** * @see Logger::error() @@ -96,13 +95,7 @@ class Logger extends BaseObject */ public static function emergency($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->emergency($message, $context); - self::getApp()->GetProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -120,13 +113,7 @@ class Logger extends BaseObject */ public static function alert($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->alert($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -143,13 +130,7 @@ class Logger extends BaseObject */ public static function critical($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->critical($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -165,14 +146,7 @@ class Logger extends BaseObject */ public static function error($message, $context = []) { - if (!isset(self::$logger)) { - echo "not set!?\n"; - return; - } - - $stamp1 = microtime(true); self::$logger->error($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -190,13 +164,7 @@ class Logger extends BaseObject */ public static function warning($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->warning($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -211,13 +179,7 @@ class Logger extends BaseObject */ public static function notice($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->notice($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -234,13 +196,7 @@ class Logger extends BaseObject */ public static function info($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->info($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -255,13 +211,7 @@ class Logger extends BaseObject */ public static function debug($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->debug($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -275,13 +225,7 @@ class Logger extends BaseObject */ public static function log($msg, $level = LogLevel::INFO) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->log($level, $msg); - self::getApp()->getProfiler()->saveTimestamp($stamp1, "file", System::callstack()); } /** @@ -296,12 +240,10 @@ class Logger extends BaseObject */ public static function devLog($msg, $level = LogLevel::DEBUG) { - if (!isset(self::$logger)) { + if (!isset(self::$devLogger)) { return; } - $stamp1 = microtime(true); self::$devLogger->log($level, $msg); - self::getApp()->getProfiler()->saveTimestamp($stamp1, "file", System::callstack()); } } diff --git a/src/Factory/DependencyFactory.php b/src/Factory/DependencyFactory.php index acbf4bfaf7..9ada2bc5d8 100644 --- a/src/Factory/DependencyFactory.php +++ b/src/Factory/DependencyFactory.php @@ -30,7 +30,7 @@ class DependencyFactory $config = Factory\ConfigFactory::createConfig($configCache); // needed to call PConfig::init() Factory\ConfigFactory::createPConfig($configCache); - $logger = Factory\LoggerFactory::create($channel, $config); + $logger = Factory\LoggerFactory::create($channel, $config, $profiler); return new App($config, $logger, $profiler, $isBackend); } diff --git a/src/Factory/LoggerFactory.php b/src/Factory/LoggerFactory.php index d36cebdb60..274044eb29 100644 --- a/src/Factory/LoggerFactory.php +++ b/src/Factory/LoggerFactory.php @@ -5,8 +5,11 @@ namespace Friendica\Factory; use Friendica\Core\Config\Configuration; use Friendica\Core\Logger; use Friendica\Network\HTTPException\InternalServerErrorException; -use Friendica\Util\Logger\FriendicaDevelopHandler; -use Friendica\Util\Logger\Introspection; +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; @@ -21,15 +24,28 @@ use Psr\Log\LogLevel; */ class LoggerFactory { + /** + * A list of classes, which shouldn't get logged + * @var array + */ + private static $ignoreClassList = [ + Logger::class, + Profiler::class, + 'Friendica\\Util\\Logger', + ]; + /** * Creates a new PSR-3 compliant logger instances * - * @param string $channel The channel of the logger instance - * @param Configuration $config The config + * @param string $channel The channel of the logger instance + * @param Configuration $config The config + * @param Profiler $profiler The profiler of the app * * @return LoggerInterface The PSR-3 compliant logger instance + * + * @throws \Exception */ - public static function create($channel, Configuration $config) + public static function create($channel, Configuration $config, Profiler $profiler) { if(empty($config->get('system', 'debugging', false))) { $logger = new VoidLogger(); @@ -37,27 +53,40 @@ class LoggerFactory return $logger; } - switch ($config->get('system', 'logger_adapter', 'monolog')) { - case 'syslog': - $intorspector = new Introspection(LOG_DEBUG, [Logger::class, SyslogLogger::class, Profiler::class]); - $level = $config->get('system', 'loglevel'); + $introspection = new Introspection(self::$ignoreClassList); + $level = $config->get('system', 'loglevel'); + + switch ($config->get('system', 'logger_config', 'stream')) { - $logger = new SyslogLogger($channel, $intorspector, $level); - break; case 'monolog': - default: $logger = new Monolog\Logger($channel); $logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor()); $logger->pushProcessor(new Monolog\Processor\ProcessIdProcessor()); $logger->pushProcessor(new Monolog\Processor\UidProcessor()); - $logger->pushProcessor(new Introspection(LogLevel::DEBUG, [Logger::class, Profiler::class])); + $logger->pushProcessor(new IntrospectionProcessor($introspection, LogLevel::DEBUG)); $stream = $config->get('system', 'logfile'); - $level = $config->get('system', 'loglevel'); $loglevel = self::mapLegacyConfigDebugLevel((string)$level); static::addStreamHandler($logger, $stream, $loglevel); break; + + case 'syslog': + $logger = new SyslogLogger($channel, $introspection, $level); + break; + + case 'stream': + default: + $stream = $config->get('system', 'logfile'); + $logger = new StreamLogger($channel, $stream, $introspection, $level); + 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); } Logger::init($logger); @@ -75,28 +104,56 @@ class LoggerFactory * * @param string $channel The channel of the logger instance * @param Configuration $config The config + * @param Profiler $profiler The profiler of the app * * @return LoggerInterface The PSR-3 compliant logger instance + * + * @throws \Exception */ - public static function createDev($channel, Configuration $config) + public static function createDev($channel, Configuration $config, Profiler $profiler) { $debugging = $config->get('system', 'debugging'); $stream = $config->get('system', 'dlogfile'); $developerIp = $config->get('system', 'dlogip'); if (!isset($developerIp) || !$debugging) { - return null; + $logger = new VoidLogger(); + Logger::setDevLogger($logger); + return $logger; } - $logger = new Monolog\Logger($channel); - $logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor()); - $logger->pushProcessor(new Monolog\Processor\ProcessIdProcessor()); - $logger->pushProcessor(new Monolog\Processor\UidProcessor()); - $logger->pushProcessor(new Introspection(LogLevel::DEBUG, ['Friendica\\Core\\Logger'])); + $introspection = new Introspection(self::$ignoreClassList); - $logger->pushHandler(new FriendicaDevelopHandler($developerIp)); + switch ($config->get('system', 'logger_config', 'stream')) { - static::addStreamHandler($logger, $stream, LogLevel::DEBUG); + case 'monolog': + $logger = new Monolog\Logger($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($channel, $introspection, LogLevel::DEBUG); + break; + + case 'stream': + default: + $logger = new StreamLogger($channel, $stream, $introspection, 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); + } Logger::setDevLogger($logger); diff --git a/src/Util/Logger/Introspection.php b/src/Util/Introspection.php similarity index 56% rename from src/Util/Logger/Introspection.php rename to src/Util/Introspection.php index f99225f9a2..6eec70954d 100644 --- a/src/Util/Logger/Introspection.php +++ b/src/Util/Introspection.php @@ -1,20 +1,12 @@ level = Logger::toMonologLevel($level); - $this->skipClassesPartials = array_merge(array('Monolog\\'), $skipClassesPartials); + $this->skipClassesPartials = $skipClassesPartials; $this->skipStackFramesCount = $skipStackFramesCount; } - public function __invoke(array $record) + /** + * Adds new classes to get skipped + * @param array $classNames + */ + public function addClasses(array $classNames) { - // 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->getRecord() - ); - - return $record; + $this->skipClassesPartials = array_merge($this->skipClassesPartials, $classNames); } /** @@ -80,6 +64,7 @@ class Introspection implements ProcessorInterface * * @param array $trace The current trace array * @param int $index The index of the current hierarchy level + * * @return bool True if the class or function should get skipped, otherwise false */ private function isTraceClassOrSkippedFunction(array $trace, $index) diff --git a/src/Util/Logger/AbstractLogger.php b/src/Util/Logger/AbstractLogger.php new file mode 100644 index 0000000000..576f4bfb43 --- /dev/null +++ b/src/Util/Logger/AbstractLogger.php @@ -0,0 +1,158 @@ +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); + } + + /** + * {@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/FriendicaDevelopHandler.php b/src/Util/Logger/Monolog/DevelopHandler.php similarity index 93% rename from src/Util/Logger/FriendicaDevelopHandler.php rename to src/Util/Logger/Monolog/DevelopHandler.php index 908d7052cc..07a839345a 100644 --- a/src/Util/Logger/FriendicaDevelopHandler.php +++ b/src/Util/Logger/Monolog/DevelopHandler.php @@ -1,6 +1,6 @@ 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 new file mode 100644 index 0000000000..fdb7c52b2a --- /dev/null +++ b/src/Util/Logger/ProfilerLogger.php @@ -0,0 +1,127 @@ +logger = $logger; + $this->profiler = $profiler; + } + + /** + * {@inheritdoc} + */ + public function emergency($message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->emergency($message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } + + /** + * {@inheritdoc} + */ + public function alert($message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->alert($message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } + + /** + * {@inheritdoc} + */ + public function critical($message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->critical($message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } + + /** + * {@inheritdoc} + */ + public function error($message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->error($message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } + + /** + * {@inheritdoc} + */ + public function warning($message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->warning($message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } + + /** + * {@inheritdoc} + */ + public function notice($message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->notice($message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } + + /** + * {@inheritdoc} + */ + public function info($message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->info($message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } + + /** + * {@inheritdoc} + */ + public function debug($message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->debug($message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } + + /** + * {@inheritdoc} + */ + public function log($level, $message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->log($level, $message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } +} diff --git a/src/Util/Logger/README.md b/src/Util/Logger/README.md new file mode 100644 index 0000000000..449403194d --- /dev/null +++ b/src/Util/Logger/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/Util/Logger/StreamLogger.php b/src/Util/Logger/StreamLogger.php new file mode 100644 index 0000000000..7e52df80f1 --- /dev/null +++ b/src/Util/Logger/StreamLogger.php @@ -0,0 +1,198 @@ + 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, $level = LogLevel::DEBUG) + { + 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)); + } + } + + 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(); + + $this->stream = fopen($this->url, 'a'); + $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::localNow() . ' '; + $logMessage .= $this->channel . ' '; + $logMessage .= '[' . strtoupper($level) . ']: '; + $logMessage .= $this->psrInterpolate($message, $context) . ' '; + $logMessage .= @json_encode($context) . ' - '; + $logMessage .= @json_encode($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->createDir(); + set_error_handler([$this, 'customErrorHandler']); + $this->stream = fopen($this->url, 'ab'); + restore_error_handler(); + + if (!is_resource($this->stream)) { + $this->stream = null; + + throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened: ' . $this->errorMessage, $this->url)); + } + } + + private function createDir() + { + $dirname = null; + $pos = strpos($this->url, '://'); + if (!$pos) { + $dirname = dirname($this->url); + } + + if (substr($this->url, 0, 7) === 'file://') { + $dirname = dirname(substr($this->url, 7)); + } + + if (isset($dirname) && !is_dir($dirname)) { + set_error_handler([$this, 'customErrorHandler']); + $status = mkdir($dirname, 0777, true); + restore_error_handler(); + + if (!$status && !is_dir($dirname)) { + throw new \UnexpectedValueException(sprintf('Directory "%s" cannot get created: ' . $this->errorMessage, $dirname)); + } + } + } + + private function customErrorHandler($code, $msg) + { + $this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $msg); + } +} diff --git a/src/Util/Logger/SyslogLogger.php b/src/Util/Logger/SyslogLogger.php index 19395157d2..e21e953ac9 100644 --- a/src/Util/Logger/SyslogLogger.php +++ b/src/Util/Logger/SyslogLogger.php @@ -3,16 +3,15 @@ namespace Friendica\Util\Logger; use Friendica\Network\HTTPException\InternalServerErrorException; -use Friendica\Util\Strings; +use Friendica\Util\Introspection; use Psr\Log\InvalidArgumentException; -use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; /** * A Logger instance for syslogging (fast, but simple) * @see http://php.net/manual/en/function.syslog.php */ -class SyslogLogger implements LoggerInterface +class SyslogLogger extends AbstractLogger { const IDENT = 'Friendica'; @@ -46,12 +45,6 @@ class SyslogLogger implements LoggerInterface LOG_EMERG => 'EMERGENCY' ]; - /** - * The channel of the current process (added to each message) - * @var string - */ - private $channel; - /** * 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 @@ -75,31 +68,41 @@ class SyslogLogger implements LoggerInterface private $logLevel; /** - * The Introspector for the current call - * @var Introspection - */ - private $introspection; - - /** - * The UID of the current call - * @var string - */ - private $logUid; - - /** - * @param string $channel The output channel + * {@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) { - $this->logUid = Strings::getRandomHex(6); - $this->channel = $channel; + parent::__construct($channel, $introspection); $this->logOpts = $logOpts; $this->logFacility = $logFacility; $this->logLevel = $this->mapLevelToPriority($level); - $this->introspection = $introspection; + $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); } /** @@ -120,6 +123,14 @@ class SyslogLogger implements LoggerInterface 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 @@ -138,14 +149,6 @@ class SyslogLogger implements LoggerInterface syslog($priority, $message); } - /** - * Closes the Syslog - */ - public function close() - { - closelog(); - } - /** * Formats a log record for the syslog output * @@ -169,129 +172,4 @@ class SyslogLogger implements LoggerInterface return $logMessage; } - - /** - * 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 - */ - private 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); - } - - /** - * Adds a new entry to the syslog - * - * @param int $level - * @param string $message - * @param array $context - * - * @throws InternalServerErrorException if the syslog isn't available - */ - private function addEntry($level, $message, $context = []) - { - if ($level >= $this->logLevel) { - return; - } - - $formattedLog = $this->formatLog($level, $message, $context); - $this->write($level, $formattedLog); - } - - /** - * {@inheritdoc} - * @throws InternalServerErrorException if the syslog isn't available - */ - public function emergency($message, array $context = array()) - { - $this->addEntry(LOG_EMERG, $message, $context); - } - - /** - * {@inheritdoc} - * @throws InternalServerErrorException if the syslog isn't available - */ - public function alert($message, array $context = array()) - { - $this->addEntry(LOG_ALERT, $message, $context); - } - - /** - * {@inheritdoc} - * @throws InternalServerErrorException if the syslog isn't available - */ - public function critical($message, array $context = array()) - { - $this->addEntry(LOG_CRIT, $message, $context); - } - - /** - * {@inheritdoc} - * @throws InternalServerErrorException if the syslog isn't available - */ - public function error($message, array $context = array()) - { - $this->addEntry(LOG_ERR, $message, $context); - } - - /** - * {@inheritdoc} - * @throws InternalServerErrorException if the syslog isn't available - */ - public function warning($message, array $context = array()) - { - $this->addEntry(LOG_WARNING, $message, $context); - } - - /** - * {@inheritdoc} - * @throws InternalServerErrorException if the syslog isn't available - */ - public function notice($message, array $context = array()) - { - $this->addEntry(LOG_NOTICE, $message, $context); - } - - /** - * {@inheritdoc} - * @throws InternalServerErrorException if the syslog isn't available - */ - public function info($message, array $context = array()) - { - $this->addEntry(LOG_INFO, $message, $context); - } - - /** - * {@inheritdoc} - * @throws InternalServerErrorException if the syslog isn't available - */ - public function debug($message, array $context = array()) - { - $this->addEntry(LOG_DEBUG, $message, $context); - } - - /** - * {@inheritdoc} - * @throws InternalServerErrorException if the syslog isn't available - */ - public function log($level, $message, array $context = array()) - { - $logLevel = $this->mapLevelToPriority($level); - $this->addEntry($logLevel, $message, $context); - } } diff --git a/tests/include/ApiTest.php b/tests/include/ApiTest.php index 289b3fcea5..c43deea405 100644 --- a/tests/include/ApiTest.php +++ b/tests/include/ApiTest.php @@ -43,7 +43,7 @@ class ApiTest extends DatabaseTest Factory\DBFactory::init($configCache, $profiler, $_SERVER); $config = Factory\ConfigFactory::createConfig($configCache); Factory\ConfigFactory::createPConfig($configCache); - $logger = Factory\LoggerFactory::create('test', $config); + $logger = Factory\LoggerFactory::create('test', $config, $profiler); $this->app = new App($config, $logger, $profiler, false); parent::setUp(); diff --git a/tests/src/Database/DBATest.php b/tests/src/Database/DBATest.php index e8b9c68b19..c6f2fae6dd 100644 --- a/tests/src/Database/DBATest.php +++ b/tests/src/Database/DBATest.php @@ -20,7 +20,7 @@ class DBATest extends DatabaseTest Factory\DBFactory::init($configCache, $profiler, $_SERVER); $config = Factory\ConfigFactory::createConfig($configCache); Factory\ConfigFactory::createPConfig($configCache); - $logger = Factory\LoggerFactory::create('test', $config); + $logger = Factory\LoggerFactory::create('test', $config, $profiler); $this->app = new App($config, $logger, $profiler, false); parent::setUp(); diff --git a/tests/src/Database/DBStructureTest.php b/tests/src/Database/DBStructureTest.php index 325ad4e5e7..fde41dab01 100644 --- a/tests/src/Database/DBStructureTest.php +++ b/tests/src/Database/DBStructureTest.php @@ -20,7 +20,7 @@ class DBStructureTest extends DatabaseTest Factory\DBFactory::init($configCache, $profiler, $_SERVER); $config = Factory\ConfigFactory::createConfig($configCache); Factory\ConfigFactory::createPConfig($configCache); - $logger = Factory\LoggerFactory::create('test', $config); + $logger = Factory\LoggerFactory::create('test', $config, $profiler); $this->app = new App($config, $logger, $profiler, false); parent::setUp(); diff --git a/tests/src/Network/CurlResultTest.php b/tests/src/Network/CurlResultTest.php index bb0bcf2471..b542371b39 100644 --- a/tests/src/Network/CurlResultTest.php +++ b/tests/src/Network/CurlResultTest.php @@ -2,11 +2,20 @@ namespace Friendica\Test\src\Network; +use Friendica\Core\Logger; use Friendica\Network\CurlResult; +use Friendica\Util\Logger\VoidLogger; use PHPUnit\Framework\TestCase; class CurlResultTest extends TestCase { + protected function setUp() + { + parent::setUp(); + + Logger::init(new VoidLogger()); + } + /** * @small */ diff --git a/tests/src/Util/Logger/StreamLoggerTest.php b/tests/src/Util/Logger/StreamLoggerTest.php new file mode 100644 index 0000000000..38706231ce --- /dev/null +++ b/tests/src/Util/Logger/StreamLoggerTest.php @@ -0,0 +1,190 @@ +setUpVfsDir(); + + $this->introspection = \Mockery::mock(Introspection::class); + $this->introspection->shouldReceive('getRecord')->andReturn([ + 'file' => self::FILE, + 'line' => self::LINE, + 'function' => self::FUNC + ]); + } + + public function assertLogline($string) + { + $this->assertRegExp(self::LOGLINE, $string); + } + + public function assertLoglineNums($assertNum, $string) + { + $this->assertEquals($assertNum, preg_match_all(self::LOGLINE, $string)); + } + + public function testNormal() + { + $logfile = vfsStream::newFile('friendica.log') + ->at($this->root); + + $logger = new StreamLogger('test', $logfile->url(), $this->introspection); + $logger->emergency('working!'); + $logger->alert('working too!'); + $logger->debug('and now?'); + $logger->notice('message', ['an' => 'context']); + + $text = $logfile->getContent(); + $this->assertLogline($text); + $this->assertLoglineNums(4, $text); + } + + /** + * Test if a log entry is correctly interpolated + */ + public function testPsrInterpolate() + { + $logfile = vfsStream::newFile('friendica.log') + ->at($this->root); + + $logger = new StreamLogger('test', $logfile->url(), $this->introspection); + + $logger->emergency('A {psr} test', ['psr' => 'working']); + $logger->alert('An {array} test', ['array' => ['it', 'is', 'working']]); + $text = $logfile->getContent(); + $this->assertContains('A working test', $text); + $this->assertContains('An ["it","is","working"] test', $text); + } + + /** + * Test if a log entry contains all necessary information + */ + public function testContainsInformation() + { + $logfile = vfsStream::newFile('friendica.log') + ->at($this->root); + + $logger = new StreamLogger('test', $logfile->url(), $this->introspection); + + $logger->emergency('A test'); + + $text = $logfile->getContent(); + $this->assertContains('"process_id":' . getmypid(), $text); + $this->assertContains('"file":"' . self::FILE . '"', $text); + $this->assertContains('"line":' . self::LINE, $text); + $this->assertContains('"function":"' . self::FUNC . '"', $text); + } + + /** + * Test if the minimum level is working + */ + public function testMinimumLevel() + { + $logfile = vfsStream::newFile('friendica.log') + ->at($this->root); + + $logger = new StreamLogger('test', $logfile->url(), $this->introspection, 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 = $logfile->getContent(); + + $this->assertLoglineNums(5, $text); + } + + /** + * Test when a file isn't set + * @expectedException \LogicException + * @expectedExceptionMessage Missing stream URL. + */ + public function testNoUrl() + { + $logger = new StreamLogger('test', '', $this->introspection); + + $logger->emergency('not working'); + } + + /** + * Test when a file cannot be opened + * @expectedException \UnexpectedValueException + * @expectedExceptionMessageRegExp /The stream or file .* could not be opened: .* / + */ + public function testWrongUrl() + { + $logfile = vfsStream::newFile('friendica.log') + ->at($this->root)->chmod(0); + + $logger = new StreamLogger('test', $logfile->url(), $this->introspection); + + $logger->emergency('not working'); + } + + /** + * Test when the directory cannot get created + * @expectedException \UnexpectedValueException + * @expectedExceptionMessageRegExp /Directory .* cannot get created: .* / + */ + public function testWrongDir() + { + $logger = new StreamLogger('test', '/a/wrong/directory/file.txt', $this->introspection); + + $logger->emergency('not working'); + } + + /** + * Test when the minimum level is not valid + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageRegExp /The level ".*" is not valid./ + */ + public function testWrongMinimumLevel() + { + $logger = new StreamLogger('test', 'file.text', $this->introspection, 'NOPE'); + } + + /** + * Test when the minimum level is not valid + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageRegExp /The level ".*" is not valid./ + */ + public function testWrongLogLevel() + { + $logfile = vfsStream::newFile('friendica.log') + ->at($this->root); + + $logger = new StreamLogger('test', $logfile->url(), $this->introspection); + + $logger->log('NOPE', 'a test'); + } +}