1
0

Log.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  6. * SPDX-License-Identifier: AGPL-3.0-only
  7. */
  8. namespace OC;
  9. use Exception;
  10. use Nextcloud\LogNormalizer\Normalizer;
  11. use OC\AppFramework\Bootstrap\Coordinator;
  12. use OC\Log\ExceptionSerializer;
  13. use OCP\EventDispatcher\IEventDispatcher;
  14. use OCP\ILogger;
  15. use OCP\IUserSession;
  16. use OCP\Log\BeforeMessageLoggedEvent;
  17. use OCP\Log\IDataLogger;
  18. use OCP\Log\IFileBased;
  19. use OCP\Log\IWriter;
  20. use OCP\Support\CrashReport\IRegistry;
  21. use Throwable;
  22. use function array_merge;
  23. use function strtr;
  24. /**
  25. * logging utilities
  26. *
  27. * This is a stand in, this should be replaced by a Psr\Log\LoggerInterface
  28. * compatible logger. See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
  29. * for the full interface specification.
  30. *
  31. * MonoLog is an example implementing this interface.
  32. */
  33. class Log implements ILogger, IDataLogger {
  34. private ?bool $logConditionSatisfied = null;
  35. private ?IEventDispatcher $eventDispatcher = null;
  36. public function __construct(
  37. private IWriter $logger,
  38. private SystemConfig $config,
  39. private ?Normalizer $normalizer = null,
  40. private ?IRegistry $crashReporters = null
  41. ) {
  42. // FIXME: php8.1 allows "private Normalizer $normalizer = new Normalizer()," in initializer
  43. if ($normalizer === null) {
  44. $this->normalizer = new Normalizer();
  45. }
  46. }
  47. public function setEventDispatcher(IEventDispatcher $eventDispatcher): void {
  48. $this->eventDispatcher = $eventDispatcher;
  49. }
  50. /**
  51. * System is unusable.
  52. *
  53. * @param string $message
  54. * @param array $context
  55. */
  56. public function emergency(string $message, array $context = []): void {
  57. $this->log(ILogger::FATAL, $message, $context);
  58. }
  59. /**
  60. * Action must be taken immediately.
  61. *
  62. * Example: Entire website down, database unavailable, etc. This should
  63. * trigger the SMS alerts and wake you up.
  64. *
  65. * @param string $message
  66. * @param array $context
  67. */
  68. public function alert(string $message, array $context = []): void {
  69. $this->log(ILogger::ERROR, $message, $context);
  70. }
  71. /**
  72. * Critical conditions.
  73. *
  74. * Example: Application component unavailable, unexpected exception.
  75. *
  76. * @param string $message
  77. * @param array $context
  78. */
  79. public function critical(string $message, array $context = []): void {
  80. $this->log(ILogger::ERROR, $message, $context);
  81. }
  82. /**
  83. * Runtime errors that do not require immediate action but should typically
  84. * be logged and monitored.
  85. *
  86. * @param string $message
  87. * @param array $context
  88. */
  89. public function error(string $message, array $context = []): void {
  90. $this->log(ILogger::ERROR, $message, $context);
  91. }
  92. /**
  93. * Exceptional occurrences that are not errors.
  94. *
  95. * Example: Use of deprecated APIs, poor use of an API, undesirable things
  96. * that are not necessarily wrong.
  97. *
  98. * @param string $message
  99. * @param array $context
  100. */
  101. public function warning(string $message, array $context = []): void {
  102. $this->log(ILogger::WARN, $message, $context);
  103. }
  104. /**
  105. * Normal but significant events.
  106. *
  107. * @param string $message
  108. * @param array $context
  109. */
  110. public function notice(string $message, array $context = []): void {
  111. $this->log(ILogger::INFO, $message, $context);
  112. }
  113. /**
  114. * Interesting events.
  115. *
  116. * Example: User logs in, SQL logs.
  117. *
  118. * @param string $message
  119. * @param array $context
  120. */
  121. public function info(string $message, array $context = []): void {
  122. $this->log(ILogger::INFO, $message, $context);
  123. }
  124. /**
  125. * Detailed debug information.
  126. *
  127. * @param string $message
  128. * @param array $context
  129. */
  130. public function debug(string $message, array $context = []): void {
  131. $this->log(ILogger::DEBUG, $message, $context);
  132. }
  133. /**
  134. * Logs with an arbitrary level.
  135. *
  136. * @param int $level
  137. * @param string $message
  138. * @param array $context
  139. */
  140. public function log(int $level, string $message, array $context = []): void {
  141. $minLevel = $this->getLogLevel($context);
  142. if ($level < $minLevel
  143. && (($this->crashReporters?->hasReporters() ?? false) === false)
  144. && (($this->eventDispatcher?->hasListeners(BeforeMessageLoggedEvent::class) ?? false) === false)) {
  145. return; // no crash reporter, no listeners, we can stop for lower log level
  146. }
  147. array_walk($context, [$this->normalizer, 'format']);
  148. $app = $context['app'] ?? 'no app in context';
  149. $entry = $this->interpolateMessage($context, $message);
  150. $this->eventDispatcher?->dispatchTyped(new BeforeMessageLoggedEvent($app, $level, $entry));
  151. $hasBacktrace = isset($entry['exception']);
  152. $logBacktrace = $this->config->getValue('log.backtrace', false);
  153. if (!$hasBacktrace && $logBacktrace) {
  154. $entry['backtrace'] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
  155. }
  156. try {
  157. if ($level >= $minLevel) {
  158. $this->writeLog($app, $entry, $level);
  159. if ($this->crashReporters !== null) {
  160. $messageContext = array_merge(
  161. $context,
  162. [
  163. 'level' => $level
  164. ]
  165. );
  166. $this->crashReporters->delegateMessage($entry['message'], $messageContext);
  167. }
  168. } else {
  169. $this->crashReporters?->delegateBreadcrumb($entry['message'], 'log', $context);
  170. }
  171. } catch (Throwable $e) {
  172. // make sure we dont hard crash if logging fails
  173. }
  174. }
  175. public function getLogLevel($context): int {
  176. $logCondition = $this->config->getValue('log.condition', []);
  177. /**
  178. * check for a special log condition - this enables an increased log on
  179. * a per request/user base
  180. */
  181. if ($this->logConditionSatisfied === null) {
  182. // default to false to just process this once per request
  183. $this->logConditionSatisfied = false;
  184. if (!empty($logCondition)) {
  185. // check for secret token in the request
  186. if (isset($logCondition['shared_secret'])) {
  187. $request = \OC::$server->getRequest();
  188. if ($request->getMethod() === 'PUT' &&
  189. !str_contains($request->getHeader('Content-Type'), 'application/x-www-form-urlencoded') &&
  190. !str_contains($request->getHeader('Content-Type'), 'application/json')) {
  191. $logSecretRequest = '';
  192. } else {
  193. $logSecretRequest = $request->getParam('log_secret', '');
  194. }
  195. // if token is found in the request change set the log condition to satisfied
  196. if ($request && hash_equals($logCondition['shared_secret'], $logSecretRequest)) {
  197. $this->logConditionSatisfied = true;
  198. }
  199. }
  200. // check for user
  201. if (isset($logCondition['users'])) {
  202. $user = \OCP\Server::get(IUserSession::class)->getUser();
  203. if ($user === null) {
  204. // User is not known for this request yet
  205. $this->logConditionSatisfied = null;
  206. } elseif (in_array($user->getUID(), $logCondition['users'], true)) {
  207. // if the user matches set the log condition to satisfied
  208. $this->logConditionSatisfied = true;
  209. }
  210. }
  211. }
  212. }
  213. // if log condition is satisfied change the required log level to DEBUG
  214. if ($this->logConditionSatisfied) {
  215. return ILogger::DEBUG;
  216. }
  217. if (isset($context['app'])) {
  218. /**
  219. * check log condition based on the context of each log message
  220. * once this is met -> change the required log level to debug
  221. */
  222. if (in_array($context['app'], $logCondition['apps'] ?? [], true)) {
  223. return ILogger::DEBUG;
  224. }
  225. }
  226. $configLogLevel = $this->config->getValue('loglevel', ILogger::WARN);
  227. if (is_numeric($configLogLevel)) {
  228. return min((int)$configLogLevel, ILogger::FATAL);
  229. }
  230. // Invalid configuration, warn the user and fall back to default level of WARN
  231. error_log('Nextcloud configuration: "loglevel" is not a valid integer');
  232. return ILogger::WARN;
  233. }
  234. /**
  235. * Logs an exception very detailed
  236. *
  237. * @param Exception|Throwable $exception
  238. * @param array $context
  239. * @return void
  240. * @since 8.2.0
  241. */
  242. public function logException(Throwable $exception, array $context = []): void {
  243. $app = $context['app'] ?? 'no app in context';
  244. $level = $context['level'] ?? ILogger::ERROR;
  245. $minLevel = $this->getLogLevel($context);
  246. if ($level < $minLevel
  247. && (($this->crashReporters?->hasReporters() ?? false) === false)
  248. && (($this->eventDispatcher?->hasListeners(BeforeMessageLoggedEvent::class) ?? false) === false)) {
  249. return; // no crash reporter, no listeners, we can stop for lower log level
  250. }
  251. // if an error is raised before the autoloader is properly setup, we can't serialize exceptions
  252. try {
  253. $serializer = $this->getSerializer();
  254. } catch (Throwable $e) {
  255. $this->error("Failed to load ExceptionSerializer serializer while trying to log " . $exception->getMessage());
  256. return;
  257. }
  258. $data = $context;
  259. unset($data['app']);
  260. unset($data['level']);
  261. $data = array_merge($serializer->serializeException($exception), $data);
  262. $data = $this->interpolateMessage($data, isset($context['message']) && $context['message'] !== '' ? $context['message'] : ('Exception thrown: ' . get_class($exception)), 'CustomMessage');
  263. array_walk($context, [$this->normalizer, 'format']);
  264. $this->eventDispatcher?->dispatchTyped(new BeforeMessageLoggedEvent($app, $level, $data));
  265. try {
  266. if ($level >= $minLevel) {
  267. if (!$this->logger instanceof IFileBased) {
  268. $data = json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_UNESCAPED_SLASHES);
  269. }
  270. $this->writeLog($app, $data, $level);
  271. }
  272. $context['level'] = $level;
  273. if (!is_null($this->crashReporters)) {
  274. $this->crashReporters->delegateReport($exception, $context);
  275. }
  276. } catch (Throwable $e) {
  277. // make sure we dont hard crash if logging fails
  278. }
  279. }
  280. public function logData(string $message, array $data, array $context = []): void {
  281. $app = $context['app'] ?? 'no app in context';
  282. $level = $context['level'] ?? ILogger::ERROR;
  283. $minLevel = $this->getLogLevel($context);
  284. array_walk($context, [$this->normalizer, 'format']);
  285. try {
  286. if ($level >= $minLevel) {
  287. $data['message'] = $message;
  288. if (!$this->logger instanceof IFileBased) {
  289. $data = json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_UNESCAPED_SLASHES);
  290. }
  291. $this->writeLog($app, $data, $level);
  292. }
  293. $context['level'] = $level;
  294. } catch (Throwable $e) {
  295. // make sure we dont hard crash if logging fails
  296. error_log('Error when trying to log exception: ' . $e->getMessage() . ' ' . $e->getTraceAsString());
  297. }
  298. }
  299. /**
  300. * @param string $app
  301. * @param string|array $entry
  302. * @param int $level
  303. */
  304. protected function writeLog(string $app, $entry, int $level): void {
  305. $this->logger->write($app, $entry, $level);
  306. }
  307. public function getLogPath():string {
  308. if ($this->logger instanceof IFileBased) {
  309. return $this->logger->getLogFilePath();
  310. }
  311. throw new \RuntimeException('Log implementation has no path');
  312. }
  313. /**
  314. * Interpolate $message as defined in PSR-3
  315. *
  316. * Returns an array containing the context without the interpolated
  317. * parameters placeholders and the message as the 'message' - or
  318. * user-defined - key.
  319. */
  320. private function interpolateMessage(array $context, string $message, string $messageKey = 'message'): array {
  321. $replace = [];
  322. $usedContextKeys = [];
  323. foreach ($context as $key => $val) {
  324. $fullKey = '{' . $key . '}';
  325. $replace[$fullKey] = $val;
  326. if (str_contains($message, $fullKey)) {
  327. $usedContextKeys[$key] = true;
  328. }
  329. }
  330. return array_merge(array_diff_key($context, $usedContextKeys), [$messageKey => strtr($message, $replace)]);
  331. }
  332. /**
  333. * @throws Throwable
  334. */
  335. protected function getSerializer(): ExceptionSerializer {
  336. $serializer = new ExceptionSerializer($this->config);
  337. try {
  338. /** @var Coordinator $coordinator */
  339. $coordinator = \OCP\Server::get(Coordinator::class);
  340. foreach ($coordinator->getRegistrationContext()->getSensitiveMethods() as $registration) {
  341. $serializer->enlistSensitiveMethods($registration->getName(), $registration->getValue());
  342. }
  343. // For not every app might be initialized at this time, we cannot assume that the return value
  344. // of getSensitiveMethods() is complete. Running delegates in Coordinator::registerApps() is
  345. // not possible due to dependencies on the one hand. On the other it would work only with
  346. // adding public methods to the PsrLoggerAdapter and this class.
  347. // Thus, serializer cannot be a property.
  348. } catch (Throwable $t) {
  349. // ignore app-defined sensitive methods in this case - they weren't loaded anyway
  350. }
  351. return $serializer;
  352. }
  353. }