aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Log
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Log')
-rw-r--r--lib/private/Log/ErrorHandler.php81
-rw-r--r--lib/private/Log/Errorlog.php31
-rw-r--r--lib/private/Log/ExceptionSerializer.php308
-rw-r--r--lib/private/Log/File.php120
-rw-r--r--lib/private/Log/LogDetails.php100
-rw-r--r--lib/private/Log/LogFactory.php61
-rw-r--r--lib/private/Log/PsrLoggerAdapter.php176
-rw-r--r--lib/private/Log/Rotate.php42
-rw-r--r--lib/private/Log/Syslog.php50
-rw-r--r--lib/private/Log/Systemdlog.php66
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));
+ }
+}