normalizer = new Normalizer(); } } public function setEventDispatcher(IEventDispatcher $eventDispatcher): void { $this->eventDispatcher = $eventDispatcher; } /** * System is unusable. * * @param string $message * @param array $context */ public function emergency(string $message, array $context = []): void { $this->log(ILogger::FATAL, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param array $context */ public function alert(string $message, array $context = []): void { $this->log(ILogger::ERROR, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param array $context */ public function critical(string $message, array $context = []): void { $this->log(ILogger::ERROR, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param array $context */ public function error(string $message, array $context = []): void { $this->log(ILogger::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param array $context */ public function warning(string $message, array $context = []): void { $this->log(ILogger::WARN, $message, $context); } /** * Normal but significant events. * * @param string $message * @param array $context */ public function notice(string $message, array $context = []): void { $this->log(ILogger::INFO, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param array $context */ public function info(string $message, array $context = []): void { $this->log(ILogger::INFO, $message, $context); } /** * Detailed debug information. * * @param string $message * @param array $context */ public function debug(string $message, array $context = []): void { $this->log(ILogger::DEBUG, $message, $context); } /** * Logs with an arbitrary level. * * @param int $level * @param string $message * @param array $context */ public function log(int $level, string $message, array $context = []): void { $minLevel = $this->getLogLevel($context, $message); if ($level < $minLevel && (($this->crashReporters?->hasReporters() ?? false) === false) && (($this->eventDispatcher?->hasListeners(BeforeMessageLoggedEvent::class) ?? false) === false)) { return; // no crash reporter, no listeners, we can stop for lower log level } array_walk($context, [$this->normalizer, 'format']); $app = $context['app'] ?? 'no app in context'; $entry = $this->interpolateMessage($context, $message); $this->eventDispatcher?->dispatchTyped(new BeforeMessageLoggedEvent($app, $level, $entry)); $hasBacktrace = isset($entry['exception']); $logBacktrace = $this->config->getValue('log.backtrace', false); if (!$hasBacktrace && $logBacktrace) { $entry['backtrace'] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); } try { if ($level >= $minLevel) { $this->writeLog($app, $entry, $level); if ($this->crashReporters !== null) { $messageContext = array_merge( $context, [ 'level' => $level ] ); $this->crashReporters->delegateMessage($entry['message'], $messageContext); } } else { $this->crashReporters?->delegateBreadcrumb($entry['message'], 'log', $context); } } catch (Throwable $e) { // make sure we dont hard crash if logging fails } } public function getLogLevel(array $context, string $message): int { /** * @psalm-var array{ * shared_secret?: string, * users?: string[], * apps?: string[], * matches?: array * } $logCondition */ $logCondition = $this->config->getValue('log.condition', []); $userId = false; /** * check for a special log condition - this enables an increased log on * a per request/user base */ if ($this->logConditionSatisfied === null) { // default to false to just process this once per request $this->logConditionSatisfied = false; if (!empty($logCondition)) { // check for secret token in the request if (isset($logCondition['shared_secret']) && $this->checkLogSecret($logCondition['shared_secret'])) { $this->logConditionSatisfied = true; } // check for user if (isset($logCondition['users'])) { $user = \OCP\Server::get(IUserSession::class)->getUser(); if ($user === null) { // User is not known for this request yet $this->logConditionSatisfied = null; } elseif (in_array($user->getUID(), $logCondition['users'], true)) { // if the user matches set the log condition to satisfied $this->logConditionSatisfied = true; } else { $userId = $user->getUID(); } } } } // if log condition is satisfied change the required log level to DEBUG if ($this->logConditionSatisfied) { return ILogger::DEBUG; } if ($userId === false && isset($logCondition['matches'])) { $user = \OCP\Server::get(IUserSession::class)->getUser(); $userId = $user === null ? false : $user->getUID(); } if (isset($context['app'])) { /** * check log condition based on the context of each log message * once this is met -> change the required log level to debug */ if (in_array($context['app'], $logCondition['apps'] ?? [], true)) { return ILogger::DEBUG; } } if (!isset($logCondition['matches'])) { $configLogLevel = $this->config->getValue('loglevel', ILogger::WARN); if (is_numeric($configLogLevel)) { return min((int)$configLogLevel, ILogger::FATAL); } // Invalid configuration, warn the user and fall back to default level of WARN error_log('Nextcloud configuration: "loglevel" is not a valid integer'); return ILogger::WARN; } foreach ($logCondition['matches'] as $option) { if ( (!isset($option['shared_secret']) || $this->checkLogSecret($option['shared_secret'])) && (!isset($option['users']) || in_array($userId, $option['users'], true)) && (!isset($option['apps']) || (isset($context['app']) && in_array($context['app'], $option['apps'], true))) && (!isset($option['message']) || str_contains($message, $option['message'])) ) { if (!isset($option['apps']) && !isset($option['loglevel']) && !isset($option['message'])) { /* Only user and/or secret are listed as conditions, we can cache the result for the rest of the request */ $this->logConditionSatisfied = true; return ILogger::DEBUG; } return $option['loglevel'] ?? ILogger::DEBUG; } } return ILogger::WARN; } protected function checkLogSecret(string $conditionSecret): bool { $request = \OCP\Server::get(IRequest::class); if ($request->getMethod() === 'PUT' && !str_contains($request->getHeader('Content-Type'), 'application/x-www-form-urlencoded') && !str_contains($request->getHeader('Content-Type'), 'application/json')) { return hash_equals($conditionSecret, ''); } // if token is found in the request change set the log condition to satisfied return hash_equals($conditionSecret, $request->getParam('log_secret', '')); } /** * Logs an exception very detailed * * @param Exception|Throwable $exception * @param array $context * @return void * @since 8.2.0 */ public function logException(Throwable $exception, array $context = []): void { $app = $context['app'] ?? 'no app in context'; $level = $context['level'] ?? ILogger::ERROR; $minLevel = $this->getLogLevel($context, $context['message'] ?? $exception->getMessage()); if ($level < $minLevel && (($this->crashReporters?->hasReporters() ?? false) === false) && (($this->eventDispatcher?->hasListeners(BeforeMessageLoggedEvent::class) ?? false) === false)) { return; // no crash reporter, no listeners, we can stop for lower log level } // if an error is raised before the autoloader is properly setup, we can't serialize exceptions try { $serializer = $this->getSerializer(); } catch (Throwable $e) { $this->error("Failed to load ExceptionSerializer serializer while trying to log " . $exception->getMessage()); return; } $data = $context; unset($data['app']); unset($data['level']); $data = array_merge($serializer->serializeException($exception), $data); $data = $this->interpolateMessage($data, isset($context['message']) && $context['message'] !== '' ? $context['message'] : ('Exception thrown: ' . get_class($exception)), 'CustomMessage'); array_walk($context, [$this->normalizer, 'format']); $this->eventDispatcher?->dispatchTyped(new BeforeMessageLoggedEvent($app, $level, $data)); try { if ($level >= $minLevel) { if (!$this->logger instanceof IFileBased) { $data = json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_UNESCAPED_SLASHES); } $this->writeLog($app, $data, $level); } $context['level'] = $level; if (!is_null($this->crashReporters)) { $this->crashReporters->delegateReport($exception, $context); } } catch (Throwable $e) { // make sure we dont hard crash if logging fails } } public function logData(string $message, array $data, array $context = []): void { $app = $context['app'] ?? 'no app in context'; $level = $context['level'] ?? ILogger::ERROR; $minLevel = $this->getLogLevel($context, $message); array_walk($context, [$this->normalizer, 'format']); try { if ($level >= $minLevel) { $data['message'] = $message; if (!$this->logger instanceof IFileBased) { $data = json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_UNESCAPED_SLASHES); } $this->writeLog($app, $data, $level); } $context['level'] = $level; } catch (Throwable $e) { // make sure we dont hard crash if logging fails error_log('Error when trying to log exception: ' . $e->getMessage() . ' ' . $e->getTraceAsString()); } } /** * @param string $app * @param string|array $entry * @param int $level */ protected function writeLog(string $app, $entry, int $level): void { $this->logger->write($app, $entry, $level); } public function getLogPath():string { if ($this->logger instanceof IFileBased) { return $this->logger->getLogFilePath(); } throw new \RuntimeException('Log implementation has no path'); } /** * Interpolate $message as defined in PSR-3 * * Returns an array containing the context without the interpolated * parameters placeholders and the message as the 'message' - or * user-defined - key. */ private function interpolateMessage(array $context, string $message, string $messageKey = 'message'): array { $replace = []; $usedContextKeys = []; foreach ($context as $key => $val) { $fullKey = '{' . $key . '}'; $replace[$fullKey] = $val; if (str_contains($message, $fullKey)) { $usedContextKeys[$key] = true; } } return array_merge(array_diff_key($context, $usedContextKeys), [$messageKey => strtr($message, $replace)]); } /** * @throws Throwable */ protected function getSerializer(): ExceptionSerializer { $serializer = new ExceptionSerializer($this->config); try { /** @var Coordinator $coordinator */ $coordinator = \OCP\Server::get(Coordinator::class); foreach ($coordinator->getRegistrationContext()->getSensitiveMethods() as $registration) { $serializer->enlistSensitiveMethods($registration->getName(), $registration->getValue()); } // For not every app might be initialized at this time, we cannot assume that the return value // of getSensitiveMethods() is complete. Running delegates in Coordinator::registerApps() is // not possible due to dependencies on the one hand. On the other it would work only with // adding public methods to the PsrLoggerAdapter and this class. // Thus, serializer cannot be a property. } catch (Throwable $t) { // ignore app-defined sensitive methods in this case - they weren't loaded anyway } return $serializer; } }