You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

282 lines
8.4 KiB

3 years ago
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\HttpKernel\DataCollector;
  11. use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext;
  12. use Symfony\Component\HttpFoundation\Request;
  13. use Symfony\Component\HttpFoundation\RequestStack;
  14. use Symfony\Component\HttpFoundation\Response;
  15. use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
  16. /**
  17. * LogDataCollector.
  18. *
  19. * @author Fabien Potencier <fabien@symfony.com>
  20. *
  21. * @final
  22. */
  23. class LoggerDataCollector extends DataCollector implements LateDataCollectorInterface
  24. {
  25. private $logger;
  26. private $containerPathPrefix;
  27. private $currentRequest;
  28. private $requestStack;
  29. public function __construct($logger = null, string $containerPathPrefix = null, RequestStack $requestStack = null)
  30. {
  31. if (null !== $logger && $logger instanceof DebugLoggerInterface) {
  32. $this->logger = $logger;
  33. }
  34. $this->containerPathPrefix = $containerPathPrefix;
  35. $this->requestStack = $requestStack;
  36. }
  37. /**
  38. * {@inheritdoc}
  39. */
  40. public function collect(Request $request, Response $response, \Throwable $exception = null)
  41. {
  42. $this->currentRequest = $this->requestStack && $this->requestStack->getMainRequest() !== $request ? $request : null;
  43. }
  44. /**
  45. * {@inheritdoc}
  46. */
  47. public function reset()
  48. {
  49. if ($this->logger instanceof DebugLoggerInterface) {
  50. $this->logger->clear();
  51. }
  52. $this->data = [];
  53. }
  54. /**
  55. * {@inheritdoc}
  56. */
  57. public function lateCollect()
  58. {
  59. if (null !== $this->logger) {
  60. $containerDeprecationLogs = $this->getContainerDeprecationLogs();
  61. $this->data = $this->computeErrorsCount($containerDeprecationLogs);
  62. // get compiler logs later (only when they are needed) to improve performance
  63. $this->data['compiler_logs'] = [];
  64. $this->data['compiler_logs_filepath'] = $this->containerPathPrefix.'Compiler.log';
  65. $this->data['logs'] = $this->sanitizeLogs(array_merge($this->logger->getLogs($this->currentRequest), $containerDeprecationLogs));
  66. $this->data = $this->cloneVar($this->data);
  67. }
  68. $this->currentRequest = null;
  69. }
  70. public function getLogs()
  71. {
  72. return $this->data['logs'] ?? [];
  73. }
  74. public function getPriorities()
  75. {
  76. return $this->data['priorities'] ?? [];
  77. }
  78. public function countErrors()
  79. {
  80. return $this->data['error_count'] ?? 0;
  81. }
  82. public function countDeprecations()
  83. {
  84. return $this->data['deprecation_count'] ?? 0;
  85. }
  86. public function countWarnings()
  87. {
  88. return $this->data['warning_count'] ?? 0;
  89. }
  90. public function countScreams()
  91. {
  92. return $this->data['scream_count'] ?? 0;
  93. }
  94. public function getCompilerLogs()
  95. {
  96. return $this->cloneVar($this->getContainerCompilerLogs($this->data['compiler_logs_filepath'] ?? null));
  97. }
  98. /**
  99. * {@inheritdoc}
  100. */
  101. public function getName()
  102. {
  103. return 'logger';
  104. }
  105. private function getContainerDeprecationLogs(): array
  106. {
  107. if (null === $this->containerPathPrefix || !is_file($file = $this->containerPathPrefix.'Deprecations.log')) {
  108. return [];
  109. }
  110. if ('' === $logContent = trim(file_get_contents($file))) {
  111. return [];
  112. }
  113. $bootTime = filemtime($file);
  114. $logs = [];
  115. foreach (unserialize($logContent) as $log) {
  116. $log['context'] = ['exception' => new SilencedErrorContext($log['type'], $log['file'], $log['line'], $log['trace'], $log['count'])];
  117. $log['timestamp'] = $bootTime;
  118. $log['priority'] = 100;
  119. $log['priorityName'] = 'DEBUG';
  120. $log['channel'] = null;
  121. $log['scream'] = false;
  122. unset($log['type'], $log['file'], $log['line'], $log['trace'], $log['trace'], $log['count']);
  123. $logs[] = $log;
  124. }
  125. return $logs;
  126. }
  127. private function getContainerCompilerLogs(string $compilerLogsFilepath = null): array
  128. {
  129. if (!is_file($compilerLogsFilepath)) {
  130. return [];
  131. }
  132. $logs = [];
  133. foreach (file($compilerLogsFilepath, \FILE_IGNORE_NEW_LINES) as $log) {
  134. $log = explode(': ', $log, 2);
  135. if (!isset($log[1]) || !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)++$/', $log[0])) {
  136. $log = ['Unknown Compiler Pass', implode(': ', $log)];
  137. }
  138. $logs[$log[0]][] = ['message' => $log[1]];
  139. }
  140. return $logs;
  141. }
  142. private function sanitizeLogs(array $logs)
  143. {
  144. $sanitizedLogs = [];
  145. $silencedLogs = [];
  146. foreach ($logs as $log) {
  147. if (!$this->isSilencedOrDeprecationErrorLog($log)) {
  148. $sanitizedLogs[] = $log;
  149. continue;
  150. }
  151. $message = '_'.$log['message'];
  152. $exception = $log['context']['exception'];
  153. if ($exception instanceof SilencedErrorContext) {
  154. if (isset($silencedLogs[$h = spl_object_hash($exception)])) {
  155. continue;
  156. }
  157. $silencedLogs[$h] = true;
  158. if (!isset($sanitizedLogs[$message])) {
  159. $sanitizedLogs[$message] = $log + [
  160. 'errorCount' => 0,
  161. 'scream' => true,
  162. ];
  163. }
  164. $sanitizedLogs[$message]['errorCount'] += $exception->count;
  165. continue;
  166. }
  167. $errorId = md5("{$exception->getSeverity()}/{$exception->getLine()}/{$exception->getFile()}\0{$message}", true);
  168. if (isset($sanitizedLogs[$errorId])) {
  169. ++$sanitizedLogs[$errorId]['errorCount'];
  170. } else {
  171. $log += [
  172. 'errorCount' => 1,
  173. 'scream' => false,
  174. ];
  175. $sanitizedLogs[$errorId] = $log;
  176. }
  177. }
  178. return array_values($sanitizedLogs);
  179. }
  180. private function isSilencedOrDeprecationErrorLog(array $log): bool
  181. {
  182. if (!isset($log['context']['exception'])) {
  183. return false;
  184. }
  185. $exception = $log['context']['exception'];
  186. if ($exception instanceof SilencedErrorContext) {
  187. return true;
  188. }
  189. if ($exception instanceof \ErrorException && \in_array($exception->getSeverity(), [\E_DEPRECATED, \E_USER_DEPRECATED], true)) {
  190. return true;
  191. }
  192. return false;
  193. }
  194. private function computeErrorsCount(array $containerDeprecationLogs): array
  195. {
  196. $silencedLogs = [];
  197. $count = [
  198. 'error_count' => $this->logger->countErrors($this->currentRequest),
  199. 'deprecation_count' => 0,
  200. 'warning_count' => 0,
  201. 'scream_count' => 0,
  202. 'priorities' => [],
  203. ];
  204. foreach ($this->logger->getLogs($this->currentRequest) as $log) {
  205. if (isset($count['priorities'][$log['priority']])) {
  206. ++$count['priorities'][$log['priority']]['count'];
  207. } else {
  208. $count['priorities'][$log['priority']] = [
  209. 'count' => 1,
  210. 'name' => $log['priorityName'],
  211. ];
  212. }
  213. if ('WARNING' === $log['priorityName']) {
  214. ++$count['warning_count'];
  215. }
  216. if ($this->isSilencedOrDeprecationErrorLog($log)) {
  217. $exception = $log['context']['exception'];
  218. if ($exception instanceof SilencedErrorContext) {
  219. if (isset($silencedLogs[$h = spl_object_hash($exception)])) {
  220. continue;
  221. }
  222. $silencedLogs[$h] = true;
  223. $count['scream_count'] += $exception->count;
  224. } else {
  225. ++$count['deprecation_count'];
  226. }
  227. }
  228. }
  229. foreach ($containerDeprecationLogs as $deprecationLog) {
  230. $count['deprecation_count'] += $deprecationLog['context']['exception']->count;
  231. }
  232. ksort($count['priorities']);
  233. return $count;
  234. }
  235. }