diff options
Diffstat (limited to 'lib/private/Log')
-rw-r--r-- | lib/private/Log/ErrorHandler.php | 81 | ||||
-rw-r--r-- | lib/private/Log/Errorlog.php | 31 | ||||
-rw-r--r-- | lib/private/Log/ExceptionSerializer.php | 308 | ||||
-rw-r--r-- | lib/private/Log/File.php | 120 | ||||
-rw-r--r-- | lib/private/Log/LogDetails.php | 100 | ||||
-rw-r--r-- | lib/private/Log/LogFactory.php | 61 | ||||
-rw-r--r-- | lib/private/Log/PsrLoggerAdapter.php | 176 | ||||
-rw-r--r-- | lib/private/Log/Rotate.php | 42 | ||||
-rw-r--r-- | lib/private/Log/Syslog.php | 50 | ||||
-rw-r--r-- | lib/private/Log/Systemdlog.php | 66 |
10 files changed, 1035 insertions, 0 deletions
diff --git a/lib/private/Log/ErrorHandler.php b/lib/private/Log/ErrorHandler.php new file mode 100644 index 00000000000..6597274a868 --- /dev/null +++ b/lib/private/Log/ErrorHandler.php @@ -0,0 +1,81 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Log; + +use Error; +use OCP\ILogger; +use Psr\Log\LoggerInterface; +use Throwable; + +class ErrorHandler { + public function __construct( + private LoggerInterface $logger, + ) { + } + + /** + * Remove password in URLs + */ + private static function removePassword(string $msg): string { + return preg_replace('#//(.*):(.*)@#', '//xxx:xxx@', $msg); + } + + /** + * Fatal errors handler + */ + public function onShutdown(): void { + $error = error_get_last(); + if ($error) { + $msg = $error['message'] . ' at ' . $error['file'] . '#' . $error['line']; + $this->logger->critical(self::removePassword($msg), ['app' => 'PHP']); + } + } + + /** + * Uncaught exception handler + */ + public function onException(Throwable $exception): void { + $class = get_class($exception); + $msg = $exception->getMessage(); + $msg = "$class: $msg at " . $exception->getFile() . '#' . $exception->getLine(); + $this->logger->critical(self::removePassword($msg), ['app' => 'PHP']); + } + + /** + * Recoverable errors handler + */ + public function onError(int $number, string $message, string $file, int $line): bool { + if (!(error_reporting() & $number)) { + return true; + } + $msg = $message . ' at ' . $file . '#' . $line; + $e = new Error(self::removePassword($msg)); + $this->logger->log(self::errnoToLogLevel($number), $e->getMessage(), ['app' => 'PHP']); + return true; + } + + /** + * Recoverable handler which catch all errors, warnings and notices + */ + public function onAll(int $number, string $message, string $file, int $line): bool { + $msg = $message . ' at ' . $file . '#' . $line; + $e = new Error(self::removePassword($msg)); + $this->logger->log(self::errnoToLogLevel($number), $e->getMessage(), ['app' => 'PHP']); + return true; + } + + private static function errnoToLogLevel(int $errno): int { + return match ($errno) { + E_WARNING, E_USER_WARNING => ILogger::WARN, + E_DEPRECATED, E_USER_DEPRECATED => ILogger::DEBUG, + E_NOTICE, E_USER_NOTICE => ILogger::INFO, + default => ILogger::ERROR, + }; + } +} diff --git a/lib/private/Log/Errorlog.php b/lib/private/Log/Errorlog.php new file mode 100644 index 00000000000..6188bb70fd5 --- /dev/null +++ b/lib/private/Log/Errorlog.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-FileCopyrightText: 2014 Christian Kampka <christian@kampka.net> + * SPDX-License-Identifier: MIT + */ +namespace OC\Log; + +use OC\SystemConfig; +use OCP\Log\IWriter; + +class Errorlog extends LogDetails implements IWriter { + public function __construct( + SystemConfig $config, + protected string $tag = 'nextcloud', + ) { + parent::__construct($config); + } + + /** + * Write a message in the log + * + * @param string|array $message + */ + public function write(string $app, $message, int $level): void { + error_log('[' . $this->tag . '][' . $app . '][' . $level . '] ' . $this->logDetailsAsJSON($app, $message, $level)); + } +} diff --git a/lib/private/Log/ExceptionSerializer.php b/lib/private/Log/ExceptionSerializer.php new file mode 100644 index 00000000000..af7c9e48435 --- /dev/null +++ b/lib/private/Log/ExceptionSerializer.php @@ -0,0 +1,308 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Log; + +use OC\Core\Controller\SetupController; +use OC\Http\Client\Client; +use OC\Security\IdentityProof\Key; +use OC\Setup; +use OC\SystemConfig; +use OCA\Encryption\Controller\RecoveryController; +use OCA\Encryption\Controller\SettingsController; +use OCA\Encryption\Crypto\Crypt; +use OCA\Encryption\Crypto\Encryption; +use OCA\Encryption\KeyManager; +use OCA\Encryption\Listeners\UserEventsListener; +use OCA\Encryption\Services\PassphraseService; +use OCA\Encryption\Session; +use OCP\HintException; + +class ExceptionSerializer { + public const SENSITIVE_VALUE_PLACEHOLDER = '*** sensitive parameters replaced ***'; + + public const methodsWithSensitiveParameters = [ + // Session/User + 'completeLogin', + 'login', + 'checkPassword', + 'checkPasswordNoLogging', + 'loginWithPassword', + 'updatePrivateKeyPassword', + 'validateUserPass', + 'loginWithToken', + '{closure}', + '{closure:*', + 'createSessionToken', + + // Provisioning + 'addUser', + + // TokenProvider + 'getToken', + 'isTokenPassword', + 'getPassword', + 'decryptPassword', + 'logClientIn', + 'generateToken', + 'validateToken', + + // TwoFactorAuth + 'solveChallenge', + 'verifyChallenge', + + // ICrypto + 'calculateHMAC', + 'encrypt', + 'decrypt', + + // LoginController + 'tryLogin', + 'confirmPassword', + + // LDAP + 'bind', + 'areCredentialsValid', + 'invokeLDAPMethod', + + // Encryption + 'storeKeyPair', + 'setupUser', + 'checkSignature', + + // files_external: OCA\Files_External\MountConfig + 'getBackendStatus', + + // files_external: UserStoragesController + 'update', + + // Preview providers, don't log big data strings + 'imagecreatefromstring', + + // text: PublicSessionController, SessionController and ApiService + 'create', + 'close', + 'push', + 'sync', + 'updateSession', + 'mention', + 'loginSessionUser', + + ]; + + public function __construct( + private SystemConfig $systemConfig, + ) { + } + + protected array $methodsWithSensitiveParametersByClass = [ + SetupController::class => [ + 'run', + 'display', + 'loadAutoConfig', + ], + Setup::class => [ + 'install' + ], + Key::class => [ + '__construct' + ], + Client::class => [ + 'request', + 'delete', + 'deleteAsync', + 'get', + 'getAsync', + 'head', + 'headAsync', + 'options', + 'optionsAsync', + 'patch', + 'post', + 'postAsync', + 'put', + 'putAsync', + ], + \Redis::class => [ + 'auth' + ], + \RedisCluster::class => [ + '__construct' + ], + Crypt::class => [ + 'symmetricEncryptFileContent', + 'encrypt', + 'generatePasswordHash', + 'encryptPrivateKey', + 'decryptPrivateKey', + 'isValidPrivateKey', + 'symmetricDecryptFileContent', + 'checkSignature', + 'createSignature', + 'decrypt', + 'multiKeyDecrypt', + 'multiKeyEncrypt', + ], + RecoveryController::class => [ + 'adminRecovery', + 'changeRecoveryPassword' + ], + SettingsController::class => [ + 'updatePrivateKeyPassword', + ], + Encryption::class => [ + 'encrypt', + 'decrypt', + ], + KeyManager::class => [ + 'checkRecoveryPassword', + 'storeKeyPair', + 'setRecoveryKey', + 'setPrivateKey', + 'setFileKey', + 'setAllFileKeys', + ], + Session::class => [ + 'setPrivateKey', + 'prepareDecryptAll', + ], + \OCA\Encryption\Users\Setup::class => [ + 'setupUser', + ], + UserEventsListener::class => [ + 'handle', + 'onUserCreated', + 'onUserLogin', + 'onBeforePasswordUpdated', + 'onPasswordUpdated', + 'onPasswordReset', + ], + PassphraseService::class => [ + 'setPassphraseForUser', + ], + ]; + + private function editTrace(array &$sensitiveValues, array $traceLine): array { + if (isset($traceLine['args'])) { + $sensitiveValues = array_merge($sensitiveValues, $traceLine['args']); + } + $traceLine['args'] = [self::SENSITIVE_VALUE_PLACEHOLDER]; + return $traceLine; + } + + private function filterTrace(array $trace) { + $sensitiveValues = []; + $trace = array_map(function (array $traceLine) use (&$sensitiveValues) { + $className = $traceLine['class'] ?? ''; + if ($className && isset($this->methodsWithSensitiveParametersByClass[$className]) + && in_array($traceLine['function'], $this->methodsWithSensitiveParametersByClass[$className], true)) { + return $this->editTrace($sensitiveValues, $traceLine); + } + foreach (self::methodsWithSensitiveParameters as $sensitiveMethod) { + if (str_contains($traceLine['function'], $sensitiveMethod) + || (str_ends_with($sensitiveMethod, '*') + && str_starts_with($traceLine['function'], substr($sensitiveMethod, 0, -1)))) { + return $this->editTrace($sensitiveValues, $traceLine); + } + } + return $traceLine; + }, $trace); + return array_map(function (array $traceLine) use ($sensitiveValues) { + if (isset($traceLine['args'])) { + $traceLine['args'] = $this->removeValuesFromArgs($traceLine['args'], $sensitiveValues); + } + return $traceLine; + }, $trace); + } + + private function removeValuesFromArgs($args, $values): array { + $workArgs = []; + foreach ($args as $key => $arg) { + if (in_array($arg, $values, true)) { + $arg = self::SENSITIVE_VALUE_PLACEHOLDER; + } elseif (is_array($arg)) { + $arg = $this->removeValuesFromArgs($arg, $values); + } + $workArgs[$key] = $arg; + } + return $workArgs; + } + + private function encodeTrace($trace) { + $trace = array_map(function (array $line) { + if (isset($line['args'])) { + $line['args'] = array_map([$this, 'encodeArg'], $line['args']); + } + return $line; + }, $trace); + return $this->filterTrace($trace); + } + + private function encodeArg($arg, $nestingLevel = 5) { + if (is_object($arg)) { + if ($nestingLevel === 0) { + return [ + '__class__' => get_class($arg), + '__properties__' => 'Encoding skipped as the maximum nesting level was reached', + ]; + } + + $objectInfo = [ '__class__' => get_class($arg) ]; + $objectVars = get_object_vars($arg); + return array_map(function ($arg) use ($nestingLevel) { + return $this->encodeArg($arg, $nestingLevel - 1); + }, array_merge($objectInfo, $objectVars)); + } + + if (is_array($arg)) { + if ($nestingLevel === 0) { + return ['Encoding skipped as the maximum nesting level was reached']; + } + + // Only log the first 5 elements of an array unless we are on debug + if ((int)$this->systemConfig->getValue('loglevel', 2) !== 0) { + $elemCount = count($arg); + if ($elemCount > 5) { + $arg = array_slice($arg, 0, 5); + $arg[] = 'And ' . ($elemCount - 5) . ' more entries, set log level to debug to see all entries'; + } + } + return array_map(function ($e) use ($nestingLevel) { + return $this->encodeArg($e, $nestingLevel - 1); + }, $arg); + } + + return $arg; + } + + public function serializeException(\Throwable $exception): array { + $data = [ + 'Exception' => get_class($exception), + 'Message' => $exception->getMessage(), + 'Code' => $exception->getCode(), + 'Trace' => $this->encodeTrace($exception->getTrace()), + 'File' => $exception->getFile(), + 'Line' => $exception->getLine(), + ]; + + if ($exception instanceof HintException) { + $data['Hint'] = $exception->getHint(); + } + + if ($exception->getPrevious()) { + $data['Previous'] = $this->serializeException($exception->getPrevious()); + } + + return $data; + } + + public function enlistSensitiveMethods(string $class, array $methods): void { + if (!isset($this->methodsWithSensitiveParametersByClass[$class])) { + $this->methodsWithSensitiveParametersByClass[$class] = []; + } + $this->methodsWithSensitiveParametersByClass[$class] = array_merge($this->methodsWithSensitiveParametersByClass[$class], $methods); + } +} diff --git a/lib/private/Log/File.php b/lib/private/Log/File.php new file mode 100644 index 00000000000..ba428ba185b --- /dev/null +++ b/lib/private/Log/File.php @@ -0,0 +1,120 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Log; + +use OC\SystemConfig; +use OCP\ILogger; +use OCP\Log\IFileBased; +use OCP\Log\IWriter; + +/** + * logging utilities + * + * Log is saved at data/nextcloud.log (on default) + */ + +class File extends LogDetails implements IWriter, IFileBased { + protected string $logFile; + + protected int $logFileMode; + + public function __construct( + string $path, + string $fallbackPath, + private SystemConfig $config, + ) { + parent::__construct($config); + $this->logFile = $path; + if (!file_exists($this->logFile)) { + if ( + ( + !is_writable(dirname($this->logFile)) + || !touch($this->logFile) + ) + && $fallbackPath !== '' + ) { + $this->logFile = $fallbackPath; + } + } + $this->logFileMode = $config->getValue('logfilemode', 0640); + } + + /** + * write a message in the log + * @param string|array $message + */ + public function write(string $app, $message, int $level): void { + $entry = $this->logDetailsAsJSON($app, $message, $level); + $handle = @fopen($this->logFile, 'a'); + if ($this->logFileMode > 0 && is_file($this->logFile) && (fileperms($this->logFile) & 0777) != $this->logFileMode) { + @chmod($this->logFile, $this->logFileMode); + } + if ($handle) { + fwrite($handle, $entry . "\n"); + fclose($handle); + } else { + // Fall back to error_log + error_log($entry); + } + if (php_sapi_name() === 'cli-server') { + if (!\is_string($message)) { + $message = json_encode($message); + } + error_log($message, 4); + } + } + + /** + * get entries from the log in reverse chronological order + */ + public function getEntries(int $limit = 50, int $offset = 0): array { + $minLevel = $this->config->getValue('loglevel', ILogger::WARN); + $entries = []; + $handle = @fopen($this->logFile, 'rb'); + if ($handle) { + fseek($handle, 0, SEEK_END); + $pos = ftell($handle); + $line = ''; + $entriesCount = 0; + $lines = 0; + // Loop through each character of the file looking for new lines + while ($pos >= 0 && ($limit === null || $entriesCount < $limit)) { + fseek($handle, $pos); + $ch = fgetc($handle); + if ($ch == "\n" || $pos == 0) { + if ($line != '') { + // Add the first character if at the start of the file, + // because it doesn't hit the else in the loop + if ($pos == 0) { + $line = $ch . $line; + } + $entry = json_decode($line); + // Add the line as an entry if it is passed the offset and is equal or above the log level + if ($entry->level >= $minLevel) { + $lines++; + if ($lines > $offset) { + $entries[] = $entry; + $entriesCount++; + } + } + $line = ''; + } + } else { + $line = $ch . $line; + } + $pos--; + } + fclose($handle); + } + return $entries; + } + + public function getLogFilePath():string { + return $this->logFile; + } +} diff --git a/lib/private/Log/LogDetails.php b/lib/private/Log/LogDetails.php new file mode 100644 index 00000000000..6063b25cef9 --- /dev/null +++ b/lib/private/Log/LogDetails.php @@ -0,0 +1,100 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Log; + +use OC\SystemConfig; + +abstract class LogDetails { + public function __construct( + private SystemConfig $config, + ) { + } + + public function logDetails(string $app, $message, int $level): array { + // default to ISO8601 + $format = $this->config->getValue('logdateformat', \DateTimeInterface::ATOM); + $logTimeZone = $this->config->getValue('logtimezone', 'UTC'); + try { + $timezone = new \DateTimeZone($logTimeZone); + } catch (\Exception $e) { + $timezone = new \DateTimeZone('UTC'); + } + $time = \DateTime::createFromFormat('U.u', number_format(microtime(true), 4, '.', '')); + if ($time === false) { + $time = new \DateTime('now', $timezone); + } else { + // apply timezone if $time is created from UNIX timestamp + $time->setTimezone($timezone); + } + $request = \OC::$server->getRequest(); + $reqId = $request->getId(); + $remoteAddr = $request->getRemoteAddress(); + // remove username/passwords from URLs before writing the to the log file + $time = $time->format($format); + $url = ($request->getRequestUri() !== '') ? $request->getRequestUri() : '--'; + $method = is_string($request->getMethod()) ? $request->getMethod() : '--'; + if ($this->config->getValue('installed', false)) { + $user = \OC_User::getUser() ?: '--'; + } else { + $user = '--'; + } + $userAgent = $request->getHeader('User-Agent'); + if ($userAgent === '') { + $userAgent = '--'; + } + $version = $this->config->getValue('version', ''); + $entry = compact( + 'reqId', + 'level', + 'time', + 'remoteAddr', + 'user', + 'app', + 'method', + 'url', + 'message', + 'userAgent', + 'version' + ); + $clientReqId = $request->getHeader('X-Request-Id'); + if ($clientReqId !== '') { + $entry['clientReqId'] = $clientReqId; + } + + if (is_array($message)) { + // Exception messages are extracted and the exception is put into a separate field + // anything else modern is split to 'message' (string) and + // data (array) fields + if (array_key_exists('Exception', $message)) { + $entry['exception'] = $message; + $entry['message'] = $message['CustomMessage'] !== '--' ? $message['CustomMessage'] : $message['Message']; + } else { + $entry['message'] = $message['message'] ?? '(no message provided)'; + unset($message['message']); + $entry['data'] = $message; + } + } + + return $entry; + } + + public function logDetailsAsJSON(string $app, $message, int $level): string { + $entry = $this->logDetails($app, $message, $level); + // PHP's json_encode only accept proper UTF-8 strings, loop over all + // elements to ensure that they are properly UTF-8 compliant or convert + // them manually. + foreach ($entry as $key => $value) { + if (is_string($value)) { + $testEncode = json_encode($value, JSON_UNESCAPED_SLASHES); + if ($testEncode === false) { + $entry[$key] = mb_convert_encoding($value, 'UTF-8', mb_detect_encoding($value)); + } + } + } + return json_encode($entry, JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_UNESCAPED_SLASHES); + } +} diff --git a/lib/private/Log/LogFactory.php b/lib/private/Log/LogFactory.php new file mode 100644 index 00000000000..ee6054b81f8 --- /dev/null +++ b/lib/private/Log/LogFactory.php @@ -0,0 +1,61 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Log; + +use OC\Log; +use OC\SystemConfig; +use OCP\IServerContainer; +use OCP\Log\ILogFactory; +use OCP\Log\IWriter; +use Psr\Log\LoggerInterface; + +class LogFactory implements ILogFactory { + public function __construct( + private IServerContainer $c, + private SystemConfig $systemConfig, + ) { + } + + /** + * @throws \OCP\AppFramework\QueryException + */ + public function get(string $type):IWriter { + return match (strtolower($type)) { + 'errorlog' => new Errorlog($this->systemConfig), + 'syslog' => $this->c->resolve(Syslog::class), + 'systemd' => $this->c->resolve(Systemdlog::class), + 'file' => $this->buildLogFile(), + default => $this->buildLogFile(), + }; + } + + protected function createNewLogger(string $type, string $tag, string $path): IWriter { + return match (strtolower($type)) { + 'errorlog' => new Errorlog($this->systemConfig, $tag), + 'syslog' => new Syslog($this->systemConfig, $tag), + 'systemd' => new Systemdlog($this->systemConfig, $tag), + default => $this->buildLogFile($path), + }; + } + + public function getCustomPsrLogger(string $path, string $type = 'file', string $tag = 'Nextcloud'): LoggerInterface { + $log = $this->createNewLogger($type, $tag, $path); + return new PsrLoggerAdapter( + new Log($log, $this->systemConfig) + ); + } + + protected function buildLogFile(string $logFile = ''): File { + $defaultLogFile = $this->systemConfig->getValue('datadirectory', \OC::$SERVERROOT . '/data') . '/nextcloud.log'; + if ($logFile === '') { + $logFile = $this->systemConfig->getValue('logfile', $defaultLogFile); + } + $fallback = $defaultLogFile !== $logFile ? $defaultLogFile : ''; + + return new File($logFile, $fallback, $this->systemConfig); + } +} diff --git a/lib/private/Log/PsrLoggerAdapter.php b/lib/private/Log/PsrLoggerAdapter.php new file mode 100644 index 00000000000..79e9d4d9621 --- /dev/null +++ b/lib/private/Log/PsrLoggerAdapter.php @@ -0,0 +1,176 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Log; + +use OC\Log; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\ILogger; +use OCP\Log\IDataLogger; +use Psr\Log\InvalidArgumentException; +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use Throwable; +use function array_key_exists; +use function array_merge; + +final class PsrLoggerAdapter implements LoggerInterface, IDataLogger { + public function __construct( + private Log $logger, + ) { + } + + public static function logLevelToInt(string $level): int { + return match ($level) { + LogLevel::ALERT => ILogger::ERROR, + LogLevel::CRITICAL => ILogger::ERROR, + LogLevel::DEBUG => ILogger::DEBUG, + LogLevel::EMERGENCY => ILogger::FATAL, + LogLevel::ERROR => ILogger::ERROR, + LogLevel::INFO => ILogger::INFO, + LogLevel::NOTICE => ILogger::INFO, + LogLevel::WARNING => ILogger::WARN, + default => throw new InvalidArgumentException('Unsupported custom log level'), + }; + } + + public function setEventDispatcher(IEventDispatcher $eventDispatcher): void { + $this->logger->setEventDispatcher($eventDispatcher); + } + + private function containsThrowable(array $context): bool { + return array_key_exists('exception', $context) && $context['exception'] instanceof Throwable; + } + + /** + * System is unusable. + * + * @param $message + * @param mixed[] $context + */ + public function emergency($message, array $context = []): void { + $this->log(LogLevel::EMERGENCY, (string)$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 $message + * @param mixed[] $context + */ + public function alert($message, array $context = []): void { + $this->log(LogLevel::ALERT, (string)$message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param $message + * @param mixed[] $context + */ + public function critical($message, array $context = []): void { + $this->log(LogLevel::CRITICAL, (string)$message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param $message + * @param mixed[] $context + */ + public function error($message, array $context = []): void { + $this->log(LogLevel::ERROR, (string)$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 $message + * @param mixed[] $context + */ + public function warning($message, array $context = []): void { + $this->log(LogLevel::WARNING, (string)$message, $context); + } + + /** + * Normal but significant events. + * + * @param $message + * @param mixed[] $context + */ + public function notice($message, array $context = []): void { + $this->log(LogLevel::NOTICE, (string)$message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param $message + * @param mixed[] $context + */ + public function info($message, array $context = []): void { + $this->log(LogLevel::INFO, (string)$message, $context); + } + + /** + * Detailed debug information. + * + * @param $message + * @param mixed[] $context + */ + public function debug($message, array $context = []): void { + $this->log(LogLevel::DEBUG, (string)$message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param $message + * @param mixed[] $context + * + * @throws InvalidArgumentException + */ + public function log($level, $message, array $context = []): void { + if (is_string($level)) { + $level = self::logLevelToInt($level); + } + if (isset($context['level']) && is_string($context['level'])) { + $context['level'] = self::logLevelToInt($context['level']); + } + if (!is_int($level) || $level < ILogger::DEBUG || $level > ILogger::FATAL) { + throw new InvalidArgumentException('Unsupported custom log level'); + } + if ($this->containsThrowable($context)) { + $this->logger->logException($context['exception'], array_merge( + [ + 'message' => (string)$message, + 'level' => $level, + ], + $context + )); + } else { + $this->logger->log($level, (string)$message, $context); + } + } + + public function logData(string $message, array $data, array $context = []): void { + $this->logger->logData($message, $data, $context); + } +} diff --git a/lib/private/Log/Rotate.php b/lib/private/Log/Rotate.php new file mode 100644 index 00000000000..ee1593b87ac --- /dev/null +++ b/lib/private/Log/Rotate.php @@ -0,0 +1,42 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Log; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use OCP\IConfig; +use OCP\Log\RotationTrait; +use Psr\Log\LoggerInterface; + +/** + * This rotates the current logfile to a new name, this way the total log usage + * will stay limited and older entries are available for a while longer. + * For more professional log management set the 'logfile' config to a different + * location and manage that with your own tools. + */ +class Rotate extends TimedJob { + use RotationTrait; + + public function __construct(ITimeFactory $time) { + parent::__construct($time); + + $this->setInterval(3600); + } + + public function run($argument): void { + $config = \OCP\Server::get(IConfig::class); + $this->filePath = $config->getSystemValueString('logfile', $config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/nextcloud.log'); + + $this->maxSize = $config->getSystemValueInt('log_rotate_size', 100 * 1024 * 1024); + if ($this->shouldRotateBySize()) { + $rotatedFile = $this->rotate(); + $msg = 'Log file "' . $this->filePath . '" was over ' . $this->maxSize . ' bytes, moved to "' . $rotatedFile . '"'; + \OCP\Server::get(LoggerInterface::class)->info($msg, ['app' => Rotate::class]); + } + } +} diff --git a/lib/private/Log/Syslog.php b/lib/private/Log/Syslog.php new file mode 100644 index 00000000000..46214599eb8 --- /dev/null +++ b/lib/private/Log/Syslog.php @@ -0,0 +1,50 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Log; + +use OC\SystemConfig; +use OCP\ILogger; +use OCP\Log\IWriter; + +class Syslog extends LogDetails implements IWriter { + protected array $levels = [ + ILogger::DEBUG => LOG_DEBUG, + ILogger::INFO => LOG_INFO, + ILogger::WARN => LOG_WARNING, + ILogger::ERROR => LOG_ERR, + ILogger::FATAL => LOG_CRIT, + ]; + + private string $tag; + + public function __construct( + SystemConfig $config, + ?string $tag = null, + ) { + parent::__construct($config); + if ($tag === null) { + $this->tag = $config->getValue('syslog_tag', 'Nextcloud'); + } else { + $this->tag = $tag; + } + } + + public function __destruct() { + closelog(); + } + + /** + * write a message in the log + * @param string|array $message + */ + public function write(string $app, $message, int $level): void { + $syslog_level = $this->levels[$level]; + openlog($this->tag, LOG_PID | LOG_CONS, LOG_USER); + syslog($syslog_level, $this->logDetailsAsJSON($app, $message, $level)); + } +} diff --git a/lib/private/Log/Systemdlog.php b/lib/private/Log/Systemdlog.php new file mode 100644 index 00000000000..ffea0511732 --- /dev/null +++ b/lib/private/Log/Systemdlog.php @@ -0,0 +1,66 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Log; + +use OC\SystemConfig; +use OCP\HintException; +use OCP\ILogger; +use OCP\Log\IWriter; + +// The following fields are understood by systemd/journald, see +// man systemd.journal-fields. All are optional: +// MESSAGE= +// The human-readable message string for this entry. +// MESSAGE_ID= +// A 128-bit message identifier ID +// PRIORITY= +// A priority value between 0 ("emerg") and 7 ("debug") +// CODE_FILE=, CODE_LINE=, CODE_FUNC= +// The code location generating this message, if known +// ERRNO= +// The low-level Unix error number causing this entry, if any. +// SYSLOG_FACILITY=, SYSLOG_IDENTIFIER=, SYSLOG_PID= +// Syslog compatibility fields + +class Systemdlog extends LogDetails implements IWriter { + protected array $levels = [ + ILogger::DEBUG => 7, + ILogger::INFO => 6, + ILogger::WARN => 4, + ILogger::ERROR => 3, + ILogger::FATAL => 2, + ]; + + protected string $syslogId; + + public function __construct( + SystemConfig $config, + ?string $tag = null, + ) { + parent::__construct($config); + if (!function_exists('sd_journal_send')) { + throw new HintException( + 'PHP extension php-systemd is not available.', + 'Please install and enable PHP extension systemd if you wish to log to the Systemd journal.'); + } + if ($tag === null) { + $tag = $config->getValue('syslog_tag', 'Nextcloud'); + } + $this->syslogId = $tag; + } + + /** + * Write a message to the log. + * @param string|array $message + */ + public function write(string $app, $message, int $level): void { + $journal_level = $this->levels[$level]; + sd_journal_send('PRIORITY=' . $journal_level, + 'SYSLOG_IDENTIFIER=' . $this->syslogId, + 'MESSAGE=' . $this->logDetailsAsJSON($app, $message, $level)); + } +} |