diff --git a/src/Factory/LoggerFactory.php b/src/Factory/LoggerFactory.php index 340f5fa5c0..f940d9d84c 100644 --- a/src/Factory/LoggerFactory.php +++ b/src/Factory/LoggerFactory.php @@ -5,8 +5,9 @@ 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\FriendicaDevelopHandler; +use Friendica\Util\Logger\Monolog\FriendicaIntrospectionProcessor; use Friendica\Util\Logger\SyslogLogger; use Friendica\Util\Logger\VoidLogger; use Friendica\Util\Logger\WorkerLogger; @@ -39,6 +40,8 @@ class LoggerFactory * @param Configuration $config The config * * @return LoggerInterface The PSR-3 compliant logger instance + * + * @throws \Exception * @throws InternalServerErrorException */ public static function create($channel, Configuration $config) @@ -49,12 +52,13 @@ class LoggerFactory return $logger; } - $introspector = new Introspection(LogLevel::DEBUG, self::$ignoreClassList); + $introspection = new Introspection(self::$ignoreClassList); + switch ($config->get('system', 'logger_adapter', 'monolog')) { case 'syslog': $level = $config->get('system', 'loglevel'); - $logger = new SyslogLogger($channel, $introspector, $level); + $logger = new SyslogLogger($channel, $introspection, $level); break; case 'monolog': default: @@ -65,7 +69,7 @@ class LoggerFactory $logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor()); $logger->pushProcessor(new Monolog\Processor\ProcessIdProcessor()); $logger->pushProcessor(new Monolog\Processor\UidProcessor()); - $logger->pushProcessor($introspector); + $logger->pushProcessor(new FriendicaIntrospectionProcessor($introspection, LogLevel::DEBUG)); $stream = $config->get('system', 'logfile'); $level = $config->get('system', 'loglevel'); @@ -107,11 +111,13 @@ class LoggerFactory $loggerTimeZone = new \DateTimeZone('UTC'); Monolog\Logger::setTimezone($loggerTimeZone); + $introspection = new Introspection(self::$ignoreClassList); + $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, self::$ignoreClassList)); + $logger->pushProcessor(new FriendicaIntrospectionProcessor($introspection, LogLevel::DEBUG)); $logger->pushHandler(new FriendicaDevelopHandler($developerIp)); diff --git a/src/Util/Logger/Introspection.php b/src/Util/Introspection.php similarity index 54% rename from src/Util/Logger/Introspection.php rename to src/Util/Introspection.php index f99225f9a2..aa8dce446c 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); } /** @@ -79,7 +63,7 @@ class Introspection implements ProcessorInterface * Checks if the current trace class or function has to be skipped * * @param array $trace The current trace array - * @param int $index The index of the current hierarchy level + * @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/FriendicaDevelopHandler.php b/src/Util/Logger/Monolog/FriendicaDevelopHandler.php similarity index 97% rename from src/Util/Logger/FriendicaDevelopHandler.php rename to src/Util/Logger/Monolog/FriendicaDevelopHandler.php index 908d7052cc..13a4645357 100644 --- a/src/Util/Logger/FriendicaDevelopHandler.php +++ b/src/Util/Logger/Monolog/FriendicaDevelopHandler.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/StreamLogger.php b/src/Util/Logger/StreamLogger.php new file mode 100644 index 0000000000..7b9bbb3c1b --- /dev/null +++ b/src/Util/Logger/StreamLogger.php @@ -0,0 +1,301 @@ + 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' + ]; + + /** + * 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 + * + * @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; + + /** + * The Introspection for the current call + * @var Introspection + */ + private $introspection; + + /** + * The UID of the current call + * @var string + */ + private $logUid; + + /** + * @param string $channel The output channel + * @param Introspection $introspection The introspection of the current call + * @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; + $this->logOpts = $logOpts; + $this->logFacility = $logFacility; + $this->logLevel = $this->mapLevelToPriority($level); + $this->introspection = $introspection; + } + + /** + * 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('LogLevel \'' . $level . '\' isn\'t valid.'); + } + + return $this->logLevels[$level]; + } + + /** + * 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) + { + if (!openlog(self::IDENT, $this->logOpts, $this->logFacility)) { + throw new InternalServerErrorException('Can\'t open syslog for ident "' . $this->channel . '" and facility "' . $this->logFacility . '""'); + } + + syslog($priority, $message); + } + + /** + * Closes the Syslog + */ + public function close() + { + closelog(); + } + + /** + * 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 .= @json_encode($context) . ' - '; + $logMessage .= @json_encode($record); + + 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/src/Util/Logger/SyslogLogger.php b/src/Util/Logger/SyslogLogger.php index 19395157d2..5cb1f8c9e6 100644 --- a/src/Util/Logger/SyslogLogger.php +++ b/src/Util/Logger/SyslogLogger.php @@ -3,6 +3,7 @@ namespace Friendica\Util\Logger; use Friendica\Network\HTTPException\InternalServerErrorException; +use Friendica\Util\Introspection; use Friendica\Util\Strings; use Psr\Log\InvalidArgumentException; use Psr\Log\LoggerInterface; @@ -75,7 +76,7 @@ class SyslogLogger implements LoggerInterface private $logLevel; /** - * The Introspector for the current call + * The Introspection for the current call * @var Introspection */ private $introspection; @@ -87,10 +88,13 @@ class SyslogLogger implements LoggerInterface private $logUid; /** - * @param string $channel The output channel - * @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 + * @param string $channel The output channel + * @param Introspection $introspection The introspection of the current call + * @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) {