diff --git a/apps/dav/appinfo/v1/caldav.php b/apps/dav/appinfo/v1/caldav.php index 24a81e9af9400..162eb0326ac91 100644 --- a/apps/dav/appinfo/v1/caldav.php +++ b/apps/dav/appinfo/v1/caldav.php @@ -34,6 +34,7 @@ use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin; use OCA\DAV\Connector\Sabre\MaintenancePlugin; use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\Profiler\ProfilerPlugin; use OCP\Accounts\IAccountManager; use Psr\Log\LoggerInterface; @@ -116,6 +117,7 @@ $server->addPlugin(\OC::$server->query(\OCA\DAV\CalDAV\Schedule\IMipPlugin::class)); } $server->addPlugin(new ExceptionLoggerPlugin('caldav', $logger)); +$server->addPlugin(\OC::$server->get(ProfilerPlugin::class)); // And off we go! $server->exec(); diff --git a/apps/dav/appinfo/v1/carddav.php b/apps/dav/appinfo/v1/carddav.php index e7faa9314e2a6..58a9463bf4d36 100644 --- a/apps/dav/appinfo/v1/carddav.php +++ b/apps/dav/appinfo/v1/carddav.php @@ -37,6 +37,7 @@ use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin; use OCA\DAV\Connector\Sabre\MaintenancePlugin; use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\Profiler\ProfilerPlugin; use OCP\Accounts\IAccountManager; use OCP\App\IAppManager; use Psr\Log\LoggerInterface; @@ -103,6 +104,7 @@ \OC::$server->get(LoggerInterface::class) ))); $server->addPlugin(new ExceptionLoggerPlugin('carddav', \OC::$server->get(LoggerInterface::class))); +$server->addPlugin(\OC::$server->get(ProfilerPlugin::class)); // And off we go! $server->exec(); diff --git a/apps/dav/appinfo/v2/direct.php b/apps/dav/appinfo/v2/direct.php index cab6109e5e62d..6374257e1e077 100644 --- a/apps/dav/appinfo/v2/direct.php +++ b/apps/dav/appinfo/v2/direct.php @@ -25,6 +25,7 @@ * */ use \OCA\DAV\Direct\ServerFactory; +use OCA\DAV\Profiler\ProfilerPlugin; // no php execution timeout for webdav if (strpos(@ini_get('disable_functions'), 'set_time_limit') === false) { @@ -48,5 +49,6 @@ \OC::$server->getBruteForceThrottler(), \OC::$server->getRequest() ); +$server->addPlugin(\OC::$server->get(ProfilerPlugin::class)); $server->exec(); diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php index d0cc8aab5d01d..087d828a7c064 100644 --- a/apps/dav/lib/Connector/Sabre/ServerFactory.php +++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php @@ -31,6 +31,7 @@ */ namespace OCA\DAV\Connector\Sabre; +use OCA\DAV\Profiler\ProfilerPlugin; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Folder; use OCA\DAV\AppInfo\PluginManager; @@ -99,6 +100,7 @@ public function createServer(string $baseUri, $server->setBaseUri($baseUri); // Load plugins + $server->addPlugin(\OC::$server->get(ProfilerPlugin::class)); $server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin($this->config, $this->l10n)); $server->addPlugin(new \OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin($this->config)); $server->addPlugin(new \OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin()); diff --git a/apps/dav/lib/Profiler/ProfilerPlugin.php b/apps/dav/lib/Profiler/ProfilerPlugin.php index 672ca4010b7b8..720f4523ca761 100644 --- a/apps/dav/lib/Profiler/ProfilerPlugin.php +++ b/apps/dav/lib/Profiler/ProfilerPlugin.php @@ -23,25 +23,68 @@ namespace OCA\DAV\Profiler; +use OCP\AppFramework\Http\Response; +use OCP\Diagnostics\IEventLogger; use OCP\IRequest; +use OCP\Profiler\IProfiler; use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; -class ProfilerPlugin extends \Sabre\DAV\ServerPlugin { - private IRequest $request; +class ProfilerPlugin extends ServerPlugin { + private bool $finalized = false; + public function __construct( + private IRequest $request, + private IProfiler $profiler, + private IEventLogger $eventLogger, + ) { + $a = 1; + } + + public function initialize(Server $server): void { + $server->on('beforeMethod:*', [$this, 'beforeMethod'], 1); + $server->on('afterMethod:*', [$this, 'afterMethod'], 9999); + $server->on('afterResponse', [$this, 'afterResponse'], 9999); + $server->on('exception', [$this, 'exception']); + } + + public function beforeMethod(): void { + $this->eventLogger->start('dav:server:method', 'Processing dav request'); + } - public function __construct(IRequest $request) { - $this->request = $request; + public function afterMethod(RequestInterface $request, ResponseInterface $response): void { + $this->eventLogger->end('dav:server:method'); + $this->eventLogger->start('dav:server:response', 'Sending dav response'); + if ($this->profiler->isEnabled()) { + $response->addHeader('X-Debug-Token', $this->request->getId()); + } } - /** @return void */ - public function initialize(Server $server) { - $server->on('afterMethod:*', [$this, 'afterMethod']); + public function afterResponse(RequestInterface $request, ResponseInterface $response): void { + $this->eventLogger->end('dav:server:response'); + $this->finalize($response->getStatus()); } - /** @return void */ - public function afterMethod(RequestInterface $request, ResponseInterface $response) { - $response->addHeader('X-Debug-Token', $this->request->getId()); + public function exception(): void { + $this->finalize(); + } + + public function __destruct() { + // in error cases, the "afterResponse" isn't called, so we do the finalization now + $this->finalize(); + } + + public function finalize(int $status = null): void { + if ($this->finalized) { + return; + } + $this->finalized = true; + + $this->eventLogger->end('runtime'); + if ($this->profiler->isEnabled()) { + $profile = $this->profiler->collect($this->request, new Response($status ?? 0)); + $this->profiler->saveProfile($profile); + } } } diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index ba67088aa706d..1cc851229fc58 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -127,7 +127,7 @@ public function __construct(IRequest $request, string $baseUri) { $this->server->httpRequest->setUrl($this->request->getRequestUri()); $this->server->setBaseUri($this->baseUri); - $this->server->addPlugin(new ProfilerPlugin($this->request)); + $this->server->addPlugin(\OC::$server->get(ProfilerPlugin::class)); $this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig())); $this->server->addPlugin(new AnonymousOptionsPlugin()); $authPlugin = new Plugin(); @@ -361,16 +361,7 @@ function () { } public function exec() { - /** @var IEventLogger $eventLogger */ - $eventLogger = \OC::$server->get(IEventLogger::class); - $eventLogger->start('dav_server_exec', ''); $this->server->exec(); - $eventLogger->end('dav_server_exec'); - if ($this->profiler->isEnabled()) { - $eventLogger->end('runtime'); - $profile = $this->profiler->collect(\OC::$server->get(IRequest::class), new Response()); - $this->profiler->saveProfile($profile); - } } private function requestIsForSubtree(array $subTrees): bool { diff --git a/core/Command/Profiler/Clear.php b/core/Command/Profiler/Clear.php new file mode 100644 index 0000000000000..efed38c83b85f --- /dev/null +++ b/core/Command/Profiler/Clear.php @@ -0,0 +1,34 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OC\Core\Command\Profiler; + +use OC\Core\Command\Base; +use OCP\Profiler\IProfiler; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Clear extends Base { + private IProfiler $profiler; + + public function __construct(IProfiler $profiler) { + parent::__construct(); + $this->profiler = $profiler; + } + + protected function configure(): void { + $this + ->setName('profiler:clear') + ->setDescription('Remove all saved profiles'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->profiler->clear(); + + return 0; + } +} diff --git a/core/Command/Profiler/Disable.php b/core/Command/Profiler/Disable.php new file mode 100644 index 0000000000000..bbb9558a8d801 --- /dev/null +++ b/core/Command/Profiler/Disable.php @@ -0,0 +1,33 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OC\Core\Command\Profiler; + +use OCP\IConfig; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Disable extends Command { + private IConfig $config; + + public function __construct(IConfig $config) { + parent::__construct(); + $this->config = $config; + } + + protected function configure(): void { + $this + ->setName('profiler:disable') + ->setDescription('Disable profiling'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->config->setSystemValue('profiler', false); + return 0; + } +} diff --git a/core/Command/Profiler/Enable.php b/core/Command/Profiler/Enable.php new file mode 100644 index 0000000000000..1357d7312f7de --- /dev/null +++ b/core/Command/Profiler/Enable.php @@ -0,0 +1,33 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OC\Core\Command\Profiler; + +use OCP\IConfig; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Enable extends Command { + private IConfig $config; + + public function __construct(IConfig $config) { + parent::__construct(); + $this->config = $config; + } + + protected function configure(): void { + $this + ->setName('profiler:enable') + ->setDescription('Enable profiling'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->config->setSystemValue('profiler', true); + return 0; + } +} diff --git a/core/Command/Profiler/Export.php b/core/Command/Profiler/Export.php new file mode 100644 index 0000000000000..d351df9a3f95b --- /dev/null +++ b/core/Command/Profiler/Export.php @@ -0,0 +1,57 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OC\Core\Command\Profiler; + +use OC\Core\Command\Base; +use OCP\Profiler\IProfiler; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Export extends Base { + private IProfiler $profiler; + + public function __construct(IProfiler $profiler) { + parent::__construct(); + $this->profiler = $profiler; + } + + protected function configure(): void { + $this + ->setName('profiler:export') + ->setDescription('Export captured profiles as json') + ->addOption('limit', null, InputOption::VALUE_REQUIRED, 'Maximum number of profiles to export') + ->addOption('url', null, InputOption::VALUE_REQUIRED, 'Url to export profiles for') + ->addOption('since', null, InputOption::VALUE_REQUIRED, 'Minimum date for exported profiles, as unix timestamp') + ->addOption('before', null, InputOption::VALUE_REQUIRED, 'Maximum date for exported profiles, as unix timestamp') + ->addOption('token', null, InputOption::VALUE_REQUIRED, 'Export only profile for a single request token'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $since = $input->getOption('since') ? (int)$input->getOption('since') : null; + $before = $input->getOption('before') ? (int)$input->getOption('before') : null; + $limit = $input->getOption('limit') ? (int)$input->getOption('limit') : 1000; + $token = $input->getOption('token') ? $input->getOption('token') : null; + $url = $input->getOption('url'); + + if ($token) { + $profiles = [$this->profiler->loadProfile($token)]; + $profiles = array_filter($profiles); + } else { + $profiles = $this->profiler->find($url, $limit, null, $since, $before); + $profiles = array_reverse($profiles); + $profiles = array_map(function (array $profile) { + return $this->profiler->loadProfile($profile['token']); + }, $profiles); + } + + $output->writeln(json_encode($profiles)); + + return 0; + } +} diff --git a/core/Command/Profiler/ListCommand.php b/core/Command/Profiler/ListCommand.php new file mode 100644 index 0000000000000..2338a28694246 --- /dev/null +++ b/core/Command/Profiler/ListCommand.php @@ -0,0 +1,84 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OC\Core\Command\Profiler; + +use OC\Core\Command\Base; +use OC\Profiler\DataCollector\DbDataCollector; +use OC\Profiler\DataCollector\MemoryDataCollector; +use OCP\Profiler\IProfiler; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class ListCommand extends Base { + private IProfiler $profiler; + + public function __construct(IProfiler $profiler) { + parent::__construct(); + $this->profiler = $profiler; + } + + protected function configure(): void { + parent::configure(); + $this + ->setName('profiler:list') + ->setDescription('List captured profiles') + ->addOption('limit', null, InputOption::VALUE_REQUIRED, 'Maximum number of profiles to return') + ->addOption('url', null, InputOption::VALUE_REQUIRED, 'Url to list profiles for') + ->addOption('since', null, InputOption::VALUE_REQUIRED, 'Minimum date for listed profiles, as unix timestamp') + ->addOption('before', null, InputOption::VALUE_REQUIRED, 'Maximum date for listed profiles, as unix timestamp'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $since = $input->getOption('since') ? (int)$input->getOption('since') : null; + $before = $input->getOption('before') ? (int)$input->getOption('before') : null; + $limit = $input->getOption('limit') ? (int)$input->getOption('limit') : 1000; + $url = $input->getOption('url'); + + $profiles = $this->profiler->find($url, $limit, null, $since, $before); + $profiles = array_reverse($profiles); + foreach ($profiles as &$profile) { + $info = $this->profiler->loadProfile($profile['token']); + if (!$info) { + continue; + } + + /** @var ?DbDataCollector $dbCollector */ + $dbCollector = $info->getCollector('db'); + /** @var ?MemoryDataCollector $memoryCollector */ + $memoryCollector = $info->getCollector('memory'); + + if ($dbCollector) { + $profile['queries'] = count($dbCollector->getQueries()); + } else { + $profile['queries'] = '--'; + } + if ($memoryCollector) { + $profile['memory'] = $memoryCollector->getMemory(); + } else { + $profile['memory'] = '--'; + } + } + + $outputType = $input->getOption('output'); + + if ($profiles) { + if ($outputType === self::OUTPUT_FORMAT_JSON || $outputType === self::OUTPUT_FORMAT_JSON_PRETTY) { + $this->writeArrayInOutputFormat($input, $output, $profiles); + } else { + $table = new Table($output); + $table->setHeaders(array_keys($profiles[0])); + $table->setRows($profiles); + $table->render(); + } + } + + return 0; + } +} diff --git a/core/register_command.php b/core/register_command.php index d9e5dfcd775eb..b2bcad7ed4752 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -214,6 +214,12 @@ $application->add(new OC\Core\Command\Security\RemoveCertificate(\OC::$server->getCertificateManager())); $application->add(\OC::$server->get(\OC\Core\Command\Security\BruteforceAttempts::class)); $application->add(\OC::$server->get(\OC\Core\Command\Security\BruteforceResetAttempts::class)); + + $application->add(\OC::$server->get(OC\Core\Command\Profiler\Clear::class)); + $application->add(\OC::$server->get(OC\Core\Command\Profiler\Disable::class)); + $application->add(\OC::$server->get(OC\Core\Command\Profiler\Enable::class)); + $application->add(\OC::$server->get(OC\Core\Command\Profiler\Export::class)); + $application->add(\OC::$server->get(OC\Core\Command\Profiler\ListCommand::class)); } else { $application->add(\OC::$server->get(\OC\Core\Command\Maintenance\Install::class)); } diff --git a/lib/composer/composer/ClassLoader.php b/lib/composer/composer/ClassLoader.php index 7824d8f7eafe8..a72151c77c8eb 100644 --- a/lib/composer/composer/ClassLoader.php +++ b/lib/composer/composer/ClassLoader.php @@ -45,34 +45,35 @@ class ClassLoader /** @var \Closure(string):void */ private static $includeFile; - /** @var string|null */ + /** @var ?string */ private $vendorDir; // PSR-4 /** - * @var array> + * @var array[] + * @psalm-var array> */ private $prefixLengthsPsr4 = array(); /** - * @var array> + * @var array[] + * @psalm-var array> */ private $prefixDirsPsr4 = array(); /** - * @var list + * @var array[] + * @psalm-var array */ private $fallbackDirsPsr4 = array(); // PSR-0 /** - * List of PSR-0 prefixes - * - * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) - * - * @var array>> + * @var array[] + * @psalm-var array> */ private $prefixesPsr0 = array(); /** - * @var list + * @var array[] + * @psalm-var array */ private $fallbackDirsPsr0 = array(); @@ -80,7 +81,8 @@ class ClassLoader private $useIncludePath = false; /** - * @var array + * @var string[] + * @psalm-var array */ private $classMap = array(); @@ -88,20 +90,21 @@ class ClassLoader private $classMapAuthoritative = false; /** - * @var array + * @var bool[] + * @psalm-var array */ private $missingClasses = array(); - /** @var string|null */ + /** @var ?string */ private $apcuPrefix; /** - * @var array + * @var self[] */ private static $registeredLoaders = array(); /** - * @param string|null $vendorDir + * @param ?string $vendorDir */ public function __construct($vendorDir = null) { @@ -110,7 +113,7 @@ public function __construct($vendorDir = null) } /** - * @return array> + * @return string[] */ public function getPrefixes() { @@ -122,7 +125,8 @@ public function getPrefixes() } /** - * @return array> + * @return array[] + * @psalm-return array> */ public function getPrefixesPsr4() { @@ -130,7 +134,8 @@ public function getPrefixesPsr4() } /** - * @return list + * @return array[] + * @psalm-return array */ public function getFallbackDirs() { @@ -138,7 +143,8 @@ public function getFallbackDirs() } /** - * @return list + * @return array[] + * @psalm-return array */ public function getFallbackDirsPsr4() { @@ -146,7 +152,8 @@ public function getFallbackDirsPsr4() } /** - * @return array Array of classname => path + * @return string[] Array of classname => path + * @psalm-return array */ public function getClassMap() { @@ -154,7 +161,8 @@ public function getClassMap() } /** - * @param array $classMap Class to filename map + * @param string[] $classMap Class to filename map + * @psalm-param array $classMap * * @return void */ @@ -171,25 +179,24 @@ public function addClassMap(array $classMap) * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * - * @param string $prefix The prefix - * @param list|string $paths The PSR-0 root directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories * * @return void */ public function add($prefix, $paths, $prepend = false) { - $paths = (array) $paths; if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( - $paths, + (array) $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, - $paths + (array) $paths ); } @@ -198,19 +205,19 @@ public function add($prefix, $paths, $prepend = false) $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { - $this->prefixesPsr0[$first][$prefix] = $paths; + $this->prefixesPsr0[$first][$prefix] = (array) $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( - $paths, + (array) $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], - $paths + (array) $paths ); } } @@ -219,9 +226,9 @@ public function add($prefix, $paths, $prepend = false) * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param list|string $paths The PSR-4 base directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException * @@ -229,18 +236,17 @@ public function add($prefix, $paths, $prepend = false) */ public function addPsr4($prefix, $paths, $prepend = false) { - $paths = (array) $paths; if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( - $paths, + (array) $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, - $paths + (array) $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { @@ -250,18 +256,18 @@ public function addPsr4($prefix, $paths, $prepend = false) throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = $paths; + $this->prefixDirsPsr4[$prefix] = (array) $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( - $paths, + (array) $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], - $paths + (array) $paths ); } } @@ -270,8 +276,8 @@ public function addPsr4($prefix, $paths, $prepend = false) * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * - * @param string $prefix The prefix - * @param list|string $paths The PSR-0 base directories + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 base directories * * @return void */ @@ -288,8 +294,8 @@ public function set($prefix, $paths) * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param list|string $paths The PSR-4 base directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException * @@ -475,9 +481,9 @@ public function findFile($class) } /** - * Returns the currently registered loaders keyed by their corresponding vendor directories. + * Returns the currently registered loaders indexed by their corresponding vendor directories. * - * @return array + * @return self[] */ public static function getRegisteredLoaders() { diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index fb063a8208821..3bb58da220155 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -754,6 +754,7 @@ 'OC\\AppFramework\\Http\\Output' => $baseDir . '/lib/private/AppFramework/Http/Output.php', 'OC\\AppFramework\\Http\\Request' => $baseDir . '/lib/private/AppFramework/Http/Request.php', 'OC\\AppFramework\\Http\\RequestId' => $baseDir . '/lib/private/AppFramework/Http/RequestId.php', + 'OC\\AppFramework\\Http\\RequestVars' => $baseDir . '/lib/private/AppFramework/Http/RequestVars.php', 'OC\\AppFramework\\Logger' => $baseDir . '/lib/private/AppFramework/Logger.php', 'OC\\AppFramework\\Middleware\\AdditionalScriptsMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/AdditionalScriptsMiddleware.php', 'OC\\AppFramework\\Middleware\\CompressionMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/CompressionMiddleware.php', @@ -1026,6 +1027,11 @@ 'OC\\Core\\Command\\Preview\\Generate' => $baseDir . '/core/Command/Preview/Generate.php', 'OC\\Core\\Command\\Preview\\Repair' => $baseDir . '/core/Command/Preview/Repair.php', 'OC\\Core\\Command\\Preview\\ResetRenderedTexts' => $baseDir . '/core/Command/Preview/ResetRenderedTexts.php', + 'OC\\Core\\Command\\Profiler\\Clear' => $baseDir . '/core/Command/Profiler/Clear.php', + 'OC\\Core\\Command\\Profiler\\Disable' => $baseDir . '/core/Command/Profiler/Disable.php', + 'OC\\Core\\Command\\Profiler\\Enable' => $baseDir . '/core/Command/Profiler/Enable.php', + 'OC\\Core\\Command\\Profiler\\Export' => $baseDir . '/core/Command/Profiler/Export.php', + 'OC\\Core\\Command\\Profiler\\ListCommand' => $baseDir . '/core/Command/Profiler/ListCommand.php', 'OC\\Core\\Command\\Security\\BruteforceAttempts' => $baseDir . '/core/Command/Security/BruteforceAttempts.php', 'OC\\Core\\Command\\Security\\BruteforceResetAttempts' => $baseDir . '/core/Command/Security/BruteforceResetAttempts.php', 'OC\\Core\\Command\\Security\\ImportCertificate' => $baseDir . '/core/Command/Security/ImportCertificate.php', @@ -1176,7 +1182,6 @@ 'OC\\DB\\Connection' => $baseDir . '/lib/private/DB/Connection.php', 'OC\\DB\\ConnectionAdapter' => $baseDir . '/lib/private/DB/ConnectionAdapter.php', 'OC\\DB\\ConnectionFactory' => $baseDir . '/lib/private/DB/ConnectionFactory.php', - 'OC\\DB\\DbDataCollector' => $baseDir . '/lib/private/DB/DbDataCollector.php', 'OC\\DB\\Exceptions\\DbalException' => $baseDir . '/lib/private/DB/Exceptions/DbalException.php', 'OC\\DB\\MigrationException' => $baseDir . '/lib/private/DB/MigrationException.php', 'OC\\DB\\MigrationService' => $baseDir . '/lib/private/DB/MigrationService.php', @@ -1519,10 +1524,14 @@ 'OC\\Profile\\Actions\\WebsiteAction' => $baseDir . '/lib/private/Profile/Actions/WebsiteAction.php', 'OC\\Profile\\ProfileManager' => $baseDir . '/lib/private/Profile/ProfileManager.php', 'OC\\Profile\\TProfileHelper' => $baseDir . '/lib/private/Profile/TProfileHelper.php', + 'OC\\Profiler\\DataCollector\\DbDataCollector' => $baseDir . '/lib/private/Profiler/DataCollector/DbDataCollector.php', + 'OC\\Profiler\\DataCollector\\EventLoggerDataProvider' => $baseDir . '/lib/private/Profiler/DataCollector/EventLoggerDataProvider.php', + 'OC\\Profiler\\DataCollector\\HttpDataCollector' => $baseDir . '/lib/private/Profiler/DataCollector/HttpDataCollector.php', + 'OC\\Profiler\\DataCollector\\MemoryDataCollector' => $baseDir . '/lib/private/Profiler/DataCollector/MemoryDataCollector.php', + 'OC\\Profiler\\DataCollector\\RoutingDataCollector' => $baseDir . '/lib/private/Profiler/DataCollector/RoutingDataCollector.php', 'OC\\Profiler\\FileProfilerStorage' => $baseDir . '/lib/private/Profiler/FileProfilerStorage.php', 'OC\\Profiler\\Profile' => $baseDir . '/lib/private/Profiler/Profile.php', 'OC\\Profiler\\Profiler' => $baseDir . '/lib/private/Profiler/Profiler.php', - 'OC\\Profiler\\RoutingDataCollector' => $baseDir . '/lib/private/Profiler/RoutingDataCollector.php', 'OC\\RedisFactory' => $baseDir . '/lib/private/RedisFactory.php', 'OC\\Remote\\Api\\ApiBase' => $baseDir . '/lib/private/Remote/Api/ApiBase.php', 'OC\\Remote\\Api\\ApiCollection' => $baseDir . '/lib/private/Remote/Api/ApiCollection.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 35b2318c4b166..ffebef8d79c87 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -787,6 +787,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\AppFramework\\Http\\Output' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Http/Output.php', 'OC\\AppFramework\\Http\\Request' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Http/Request.php', 'OC\\AppFramework\\Http\\RequestId' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Http/RequestId.php', + 'OC\\AppFramework\\Http\\RequestVars' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Http/RequestVars.php', 'OC\\AppFramework\\Logger' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Logger.php', 'OC\\AppFramework\\Middleware\\AdditionalScriptsMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/AdditionalScriptsMiddleware.php', 'OC\\AppFramework\\Middleware\\CompressionMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/CompressionMiddleware.php', @@ -1059,6 +1060,11 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Command\\Preview\\Generate' => __DIR__ . '/../../..' . '/core/Command/Preview/Generate.php', 'OC\\Core\\Command\\Preview\\Repair' => __DIR__ . '/../../..' . '/core/Command/Preview/Repair.php', 'OC\\Core\\Command\\Preview\\ResetRenderedTexts' => __DIR__ . '/../../..' . '/core/Command/Preview/ResetRenderedTexts.php', + 'OC\\Core\\Command\\Profiler\\Clear' => __DIR__ . '/../../..' . '/core/Command/Profiler/Clear.php', + 'OC\\Core\\Command\\Profiler\\Disable' => __DIR__ . '/../../..' . '/core/Command/Profiler/Disable.php', + 'OC\\Core\\Command\\Profiler\\Enable' => __DIR__ . '/../../..' . '/core/Command/Profiler/Enable.php', + 'OC\\Core\\Command\\Profiler\\Export' => __DIR__ . '/../../..' . '/core/Command/Profiler/Export.php', + 'OC\\Core\\Command\\Profiler\\ListCommand' => __DIR__ . '/../../..' . '/core/Command/Profiler/ListCommand.php', 'OC\\Core\\Command\\Security\\BruteforceAttempts' => __DIR__ . '/../../..' . '/core/Command/Security/BruteforceAttempts.php', 'OC\\Core\\Command\\Security\\BruteforceResetAttempts' => __DIR__ . '/../../..' . '/core/Command/Security/BruteforceResetAttempts.php', 'OC\\Core\\Command\\Security\\ImportCertificate' => __DIR__ . '/../../..' . '/core/Command/Security/ImportCertificate.php', @@ -1209,7 +1215,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\DB\\Connection' => __DIR__ . '/../../..' . '/lib/private/DB/Connection.php', 'OC\\DB\\ConnectionAdapter' => __DIR__ . '/../../..' . '/lib/private/DB/ConnectionAdapter.php', 'OC\\DB\\ConnectionFactory' => __DIR__ . '/../../..' . '/lib/private/DB/ConnectionFactory.php', - 'OC\\DB\\DbDataCollector' => __DIR__ . '/../../..' . '/lib/private/DB/DbDataCollector.php', 'OC\\DB\\Exceptions\\DbalException' => __DIR__ . '/../../..' . '/lib/private/DB/Exceptions/DbalException.php', 'OC\\DB\\MigrationException' => __DIR__ . '/../../..' . '/lib/private/DB/MigrationException.php', 'OC\\DB\\MigrationService' => __DIR__ . '/../../..' . '/lib/private/DB/MigrationService.php', @@ -1552,10 +1557,14 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Profile\\Actions\\WebsiteAction' => __DIR__ . '/../../..' . '/lib/private/Profile/Actions/WebsiteAction.php', 'OC\\Profile\\ProfileManager' => __DIR__ . '/../../..' . '/lib/private/Profile/ProfileManager.php', 'OC\\Profile\\TProfileHelper' => __DIR__ . '/../../..' . '/lib/private/Profile/TProfileHelper.php', + 'OC\\Profiler\\DataCollector\\DbDataCollector' => __DIR__ . '/../../..' . '/lib/private/Profiler/DataCollector/DbDataCollector.php', + 'OC\\Profiler\\DataCollector\\EventLoggerDataProvider' => __DIR__ . '/../../..' . '/lib/private/Profiler/DataCollector/EventLoggerDataProvider.php', + 'OC\\Profiler\\DataCollector\\HttpDataCollector' => __DIR__ . '/../../..' . '/lib/private/Profiler/DataCollector/HttpDataCollector.php', + 'OC\\Profiler\\DataCollector\\MemoryDataCollector' => __DIR__ . '/../../..' . '/lib/private/Profiler/DataCollector/MemoryDataCollector.php', + 'OC\\Profiler\\DataCollector\\RoutingDataCollector' => __DIR__ . '/../../..' . '/lib/private/Profiler/DataCollector/RoutingDataCollector.php', 'OC\\Profiler\\FileProfilerStorage' => __DIR__ . '/../../..' . '/lib/private/Profiler/FileProfilerStorage.php', 'OC\\Profiler\\Profile' => __DIR__ . '/../../..' . '/lib/private/Profiler/Profile.php', 'OC\\Profiler\\Profiler' => __DIR__ . '/../../..' . '/lib/private/Profiler/Profiler.php', - 'OC\\Profiler\\RoutingDataCollector' => __DIR__ . '/../../..' . '/lib/private/Profiler/RoutingDataCollector.php', 'OC\\RedisFactory' => __DIR__ . '/../../..' . '/lib/private/RedisFactory.php', 'OC\\Remote\\Api\\ApiBase' => __DIR__ . '/../../..' . '/lib/private/Remote/Api/ApiBase.php', 'OC\\Remote\\Api\\ApiCollection' => __DIR__ . '/../../..' . '/lib/private/Remote/Api/ApiCollection.php', diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php index 88044fbf7b6f5..decbbc4cd8861 100644 --- a/lib/private/App/AppManager.php +++ b/lib/private/App/AppManager.php @@ -398,7 +398,7 @@ public function loadApp(string $app): void { $hasAppPhpFile = is_file($appPath . '/appinfo/app.php'); $eventLogger = \OC::$server->get(IEventLogger::class); - $eventLogger->start('bootstrap:load_app_' . $app, 'Load app: ' . $app); + $eventLogger->start("bootstrap:load_app:$app", 'Load app: ' . $app); if ($isBootable && $hasAppPhpFile) { $this->logger->error('/appinfo/app.php is not loaded when \OCP\AppFramework\Bootstrap\IBootstrap on the application class is used. Migrate everything from app.php to the Application class.', [ 'app' => $app, diff --git a/lib/private/AppFramework/App.php b/lib/private/AppFramework/App.php index ffd77da888ed5..0001fd3219129 100644 --- a/lib/private/AppFramework/App.php +++ b/lib/private/AppFramework/App.php @@ -34,16 +34,16 @@ use OC\AppFramework\DependencyInjection\DIContainer; use OC\AppFramework\Http\Dispatcher; use OC\AppFramework\Http\Request; +use OC\Profiler\DataCollector\RoutingDataCollector; use OCP\App\IAppManager; -use OCP\Profiler\IProfiler; -use OC\Profiler\RoutingDataCollector; -use OCP\AppFramework\QueryException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\ICallbackResponse; use OCP\AppFramework\Http\IOutput; +use OCP\AppFramework\QueryException; use OCP\Diagnostics\IEventLogger; use OCP\HintException; use OCP\IRequest; +use OCP\Profiler\IProfiler; /** * Entry point for every request in your app. You can consider this as your diff --git a/lib/private/AppFramework/Http/Request.php b/lib/private/AppFramework/Http/Request.php index 408e88583a070..cf5795319e584 100644 --- a/lib/private/AppFramework/Http/Request.php +++ b/lib/private/AppFramework/Http/Request.php @@ -78,11 +78,7 @@ class Request implements \ArrayAccess, \Countable, IRequest { public const USER_AGENT_ANDROID_MOBILE_CHROME = '#Android.*Chrome/[.0-9]*#'; public const USER_AGENT_FREEBOX = '#^Mozilla/5\.0$#'; public const REGEX_LOCALHOST = '/^(127\.0\.0\.1|localhost|\[::1\])$/'; - - protected string $inputStream; - protected $content; - protected array $items = []; - protected array $allowedKeys = [ + public const ALLOWED_KEYS = [ 'get', 'post', 'files', @@ -94,6 +90,10 @@ class Request implements \ArrayAccess, \Countable, IRequest { 'method', 'requesttoken', ]; + + protected string $inputStream; + protected $content; + protected array $items = []; protected IRequestId $requestId; protected IConfig $config; protected ?CsrfTokenManager $csrfTokenManager; @@ -132,7 +132,7 @@ public function __construct(array $vars, $vars['method'] = 'GET'; } - foreach ($this->allowedKeys as $name) { + foreach (self::ALLOWED_KEYS as $name) { $this->items[$name] = $vars[$name] ?? []; } @@ -276,7 +276,7 @@ public function __get($name) { * @return bool */ public function __isset($name) { - if (\in_array($name, $this->allowedKeys, true)) { + if (\in_array($name, self::ALLOWED_KEYS, true)) { return true; } return isset($this->items['parameters'][$name]); diff --git a/lib/private/AppFramework/Http/RequestVars.php b/lib/private/AppFramework/Http/RequestVars.php new file mode 100644 index 0000000000000..56443f5817ed6 --- /dev/null +++ b/lib/private/AppFramework/Http/RequestVars.php @@ -0,0 +1,104 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\AppFramework\Http; + +/** + * A restricted version of IRequest that only contains the request parameters + */ +class RequestVars { + private array $items = []; + + public function __construct(array $vars) { + if (!array_key_exists('method', $vars)) { + $vars['method'] = 'GET'; + } + + foreach (Request::ALLOWED_KEYS as $name) { + $this->items[$name] = $vars[$name] ?? []; + } + + $this->items['parameters'] = array_merge( + $this->items['get'], + $this->items['post'], + $this->items['urlParams'] + ); + } + + /** + * Returns the value for a specific http header. + * + * This method returns an empty string if the header did not exist. + * + * @param string $name + * @return string + */ + public function getHeader(string $name): string { + $server = $this->items['server']; + $name = strtoupper(str_replace('-', '_', $name)); + if (isset($server['HTTP_' . $name])) { + return $server['HTTP_' . $name]; + } + + // There's a few headers that seem to end up in the top-level + // server array. + switch ($name) { + case 'CONTENT_TYPE': + case 'CONTENT_LENGTH': + case 'REMOTE_ADDR': + if (isset($server[$name])) { + return $server[$name]; + } + break; + } + + return ''; + } + + /** + * Returns the method of the request + * + * @return string the method of the request (POST, GET, etc) + */ + public function getMethod(): string { + return $this->items['method']; + } + + /** + * Lets you access post and get parameters by the index + * In case of json requests the encoded json body is accessed + * + * @param string $key the key which you want to access in the URL Parameter + * placeholder, $_POST or $_GET array. + * The priority how they're returned is the following: + * 1. URL parameters + * 2. POST parameters + * 3. GET parameters + * @param mixed $default If the key is not found, this value will be returned + * @return mixed the content of the array + */ + public function getParam(string $key, $default = null) { + $parameters = $this->items['parameters']; + return $parameters[$key] ?? $default; + } +} diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php index 2bd1d4c824ac8..aba103c473d75 100644 --- a/lib/private/DB/Connection.php +++ b/lib/private/DB/Connection.php @@ -47,13 +47,14 @@ use Doctrine\DBAL\Result; use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Statement; +use OC\DB\QueryBuilder\QueryBuilder; +use OC\Profiler\DataCollector\DbDataCollector; +use OC\SystemConfig; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Diagnostics\IEventLogger; use OCP\IRequestId; use OCP\PreConditionNotMetException; use OCP\Profiler\IProfiler; -use OC\DB\QueryBuilder\QueryBuilder; -use OC\SystemConfig; use Psr\Log\LoggerInterface; class Connection extends \Doctrine\DBAL\Connection { diff --git a/lib/private/Diagnostics/EventLogger.php b/lib/private/Diagnostics/EventLogger.php index f1dd1addb08e1..bc34df7c45c83 100644 --- a/lib/private/Diagnostics/EventLogger.php +++ b/lib/private/Diagnostics/EventLogger.php @@ -25,9 +25,11 @@ namespace OC\Diagnostics; use OC\Log; +use OC\Profiler\DataCollector\EventLoggerDataProvider; use OC\SystemConfig; use OCP\Diagnostics\IEvent; use OCP\Diagnostics\IEventLogger; +use OCP\Profiler\IProfiler; use Psr\Log\LoggerInterface; class EventLogger implements IEventLogger { @@ -47,11 +49,13 @@ class EventLogger implements IEventLogger { * @var bool - Module needs to be activated by some app */ private $activated = false; + private IProfiler $profiler; - public function __construct(SystemConfig $config, LoggerInterface $logger, Log $internalLogger) { + public function __construct(SystemConfig $config, LoggerInterface $logger, Log $internalLogger, IProfiler $profiler) { $this->config = $config; $this->logger = $logger; $this->internalLogger = $internalLogger; + $this->profiler = $profiler; if ($this->isLoggingActivated()) { $this->activate(); @@ -59,15 +63,18 @@ public function __construct(SystemConfig $config, LoggerInterface $logger, Log $ } public function isLoggingActivated(): bool { - $systemValue = (bool)$this->config->getValue('diagnostics.logging', false) - || (bool)$this->config->getValue('profiler', false); + if ($this->profiler->isEnabled()) { + return true; + } - if ($systemValue && $this->config->getValue('debug', false)) { + $diagnosticsEnabled = $this->config->getValue('diagnostics.logging', false); + + if ($diagnosticsEnabled && $this->config->getValue('debug', false)) { return true; } $isDebugLevel = $this->internalLogger->getLogLevel([]) === Log::DEBUG; - return $systemValue && $isDebugLevel; + return $diagnosticsEnabled && $isDebugLevel; } /** @@ -113,6 +120,9 @@ public function getEvents() { * @inheritdoc */ public function activate() { + if (!$this->activated) { + $this->profiler->add(new EventLoggerDataProvider($this)); + } $this->activated = true; } diff --git a/lib/private/DB/DbDataCollector.php b/lib/private/Profiler/DataCollector/DbDataCollector.php similarity index 94% rename from lib/private/DB/DbDataCollector.php rename to lib/private/Profiler/DataCollector/DbDataCollector.php index 60e3dbe797dc5..02b4fac14dfc7 100644 --- a/lib/private/DB/DbDataCollector.php +++ b/lib/private/Profiler/DataCollector/DbDataCollector.php @@ -23,14 +23,18 @@ * */ -namespace OC\DB; +namespace OC\Profiler\DataCollector; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type; use OC\AppFramework\Http\Request; +use OC\DB\BacktraceDebugStack; +use OC\DB\Connection; +use OC\DB\ObjectParameter; use OCP\AppFramework\Http\Response; +use OCP\DataCollector\AbstractDataCollector; -class DbDataCollector extends \OCP\DataCollector\AbstractDataCollector { +class DbDataCollector extends AbstractDataCollector { protected ?BacktraceDebugStack $debugStack = null; private Connection $connection; diff --git a/lib/private/Profiler/DataCollector/EventLoggerDataProvider.php b/lib/private/Profiler/DataCollector/EventLoggerDataProvider.php new file mode 100644 index 0000000000000..e0d97e00558d6 --- /dev/null +++ b/lib/private/Profiler/DataCollector/EventLoggerDataProvider.php @@ -0,0 +1,38 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OC\Profiler\DataCollector; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\Response; +use OCP\DataCollector\AbstractDataCollector; +use OCP\Diagnostics\IEventLogger; + +class EventLoggerDataProvider extends AbstractDataCollector { + private IEventLogger $eventLogger; + + public function __construct(IEventLogger $eventLogger) { + $this->eventLogger = $eventLogger; + } + + public function collect(Request $request, Response $response, \Throwable $exception = null): void { + $this->data = []; + foreach ($this->eventLogger->getEvents() as $event) { + $this->data[$event->getId()] = [ + 'start' => $event->getStart(), + 'stop' => $event->getEnd(), + 'description' => $event->getDescription(), + 'duration' => $event->getDuration(), + 'id' => $event->getId(), + ]; + }; + } + + public function getName(): string { + return 'event'; + } +} diff --git a/lib/private/Profiler/DataCollector/HttpDataCollector.php b/lib/private/Profiler/DataCollector/HttpDataCollector.php new file mode 100644 index 0000000000000..08a36aaa155f2 --- /dev/null +++ b/lib/private/Profiler/DataCollector/HttpDataCollector.php @@ -0,0 +1,41 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OC\Profiler\DataCollector; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\Response; +use OCP\DataCollector\AbstractDataCollector; + +class HttpDataCollector extends AbstractDataCollector { + public function getName(): string { + return 'http'; + } + + public function collect(Request $request, Response $response, \Throwable $exception = null): void { + try { + $content = $request->getParams(); + } catch (\THrowable $ex) { + $content = null; + } + $this->data = [ + 'request' => [ + 'url' => $request->getRequestUri(), + 'method' => $request->getMethod(), + 'content' => $content, + 'httpProtocol' => $request->getHttpProtocol(), + 'userAgent' => $_SERVER['HTTP_USER_AGENT'], + 'params' => $content, + ], + 'response' => [ + 'headers' => $response->getHeaders(), + 'statusCode' => $response->getStatus(), + 'etag' => $response->getETag(), + ] + ]; + } +} diff --git a/lib/private/Profiler/DataCollector/MemoryDataCollector.php b/lib/private/Profiler/DataCollector/MemoryDataCollector.php new file mode 100644 index 0000000000000..f6e28e349f552 --- /dev/null +++ b/lib/private/Profiler/DataCollector/MemoryDataCollector.php @@ -0,0 +1,67 @@ + +// SPDX-FileCopyrightText: 2022 Carl Schwan +// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT + +namespace OC\Profiler\DataCollector; + +use OC\AppFramework\Http\Request; +use OCP\AppFramework\Http\Response; +use OCP\DataCollector\AbstractDataCollector; + +class MemoryDataCollector extends AbstractDataCollector { + public function getName(): string { + return 'memory'; + } + + public function collect(Request $request, Response $response, \Throwable $exception = null): void { + $this->data = [ + 'memory' => memory_get_peak_usage(true), + 'memory_limit' => $this->convertToBytes(ini_get('memory_limit')), + ]; + } + + public function getMemory(): int { + return $this->data['memory']; + } + + /** + * @return int|float + */ + public function getMemoryLimit() { + return $this->data['memory_limit']; + } + + /** + * @return int|float + */ + private function convertToBytes(string $memoryLimit) { + if ('-1' === $memoryLimit) { + return -1; + } + + $memoryLimit = strtolower($memoryLimit); + $max = strtolower(ltrim($memoryLimit, '+')); + if (str_starts_with($max, '0x')) { + $max = \intval($max, 16); + } elseif (str_starts_with($max, '0')) { + $max = \intval($max, 8); + } else { + $max = (int) $max; + } + + switch (substr($memoryLimit, -1)) { + case 't': $max *= 1024; + // no break + case 'g': $max *= 1024; + // no break + case 'm': $max *= 1024; + // no break + case 'k': $max *= 1024; + } + return $max; + } +} diff --git a/lib/private/Profiler/RoutingDataCollector.php b/lib/private/Profiler/DataCollector/RoutingDataCollector.php similarity index 97% rename from lib/private/Profiler/RoutingDataCollector.php rename to lib/private/Profiler/DataCollector/RoutingDataCollector.php index e665923087994..9d54ba8aea39a 100644 --- a/lib/private/Profiler/RoutingDataCollector.php +++ b/lib/private/Profiler/DataCollector/RoutingDataCollector.php @@ -24,7 +24,7 @@ * */ -namespace OC\Profiler; +namespace OC\Profiler\DataCollector; use OC\AppFramework\Http\Request; use OCP\AppFramework\Http\Response; diff --git a/lib/private/Profiler/FileProfilerStorage.php b/lib/private/Profiler/FileProfilerStorage.php index d7f3df752a649..4307cc642bd7e 100644 --- a/lib/private/Profiler/FileProfilerStorage.php +++ b/lib/private/Profiler/FileProfilerStorage.php @@ -34,6 +34,7 @@ class FileProfilerStorage { // Folder where profiler data are stored. private string $folder; + private bool $folderPrepared = false; /** * Constructs the file storage using a "dsn-like" path. @@ -44,6 +45,13 @@ class FileProfilerStorage { */ public function __construct(string $folder) { $this->folder = $folder; + } + + private function prepareFolder() { + if ($this->folderPrepared) { + return; + } + $this->folderPrepared = true; if (!is_dir($this->folder) && false === @mkdir($this->folder, 0777, true) && !is_dir($this->folder)) { throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $this->folder)); @@ -51,6 +59,7 @@ public function __construct(string $folder) { } public function find(?string $url, ?int $limit, ?string $method, int $start = null, int $end = null, string $statusCode = null): array { + $this->prepareFolder(); $file = $this->getIndexFilename(); if (!file_exists($file)) { @@ -94,21 +103,23 @@ public function find(?string $url, ?int $limit, ?string $method, int $start = nu } public function purge(): void { + $this->prepareFolder(); $flags = \FilesystemIterator::SKIP_DOTS; $iterator = new \RecursiveDirectoryIterator($this->folder, $flags); $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST); foreach ($iterator as $file) { - $file = (string)$file->getPathInfo(); - if (is_file($file)) { - unlink($file); + /** @var \SplFileInfo $file */ + if ($file->isFile()) { + unlink($file->getPathname()); } else { - rmdir($file); + rmdir($file->getPathname()); } } } public function read(string $token): ?IProfile { + $this->prepareFolder(); if (!$token || !file_exists($file = $this->getFilename($token))) { return null; } @@ -124,6 +135,7 @@ public function read(string $token): ?IProfile { * @throws \RuntimeException */ public function write(IProfile $profile): bool { + $this->prepareFolder(); $file = $this->getFilename($profile->getToken()); $profileIndexed = is_file($file); diff --git a/lib/private/Profiler/Profiler.php b/lib/private/Profiler/Profiler.php index 40050b7bf4388..00b5ce54a0095 100644 --- a/lib/private/Profiler/Profiler.php +++ b/lib/private/Profiler/Profiler.php @@ -1,6 +1,6 @@ @@ -27,8 +27,12 @@ namespace OC\Profiler; use OC\AppFramework\Http\Request; +use OC\AppFramework\Http\RequestVars; +use OC\Profiler\DataCollector\HttpDataCollector; +use OC\Profiler\DataCollector\MemoryDataCollector; use OCP\AppFramework\Http\Response; use OCP\DataCollector\IDataCollector; +use OCP\IRequest; use OCP\Profiler\IProfiler; use OCP\Profiler\IProfile; use OC\SystemConfig; @@ -41,11 +45,34 @@ class Profiler implements IProfiler { private bool $enabled = false; - public function __construct(SystemConfig $config) { - $this->enabled = $config->getValue('profiler', false); - if ($this->enabled) { - $this->storage = new FileProfilerStorage($config->getValue('datadirectory', \OC::$SERVERROOT . '/data') . '/profiler'); + /** + * we inject the container, else we get a loop with the IRequest + */ + public function __construct(SystemConfig $config, RequestVars $request) { + $this->setEnabled($this->shouldProfilerBeEnabled($config, $request)); + $this->storage = new FileProfilerStorage($config->getValue('datadirectory', \OC::$SERVERROOT . '/data') . '/profiler'); + } + + private function shouldProfilerBeEnabled(SystemConfig $config, RequestVars $request): bool { + if ($config->getValue('profiler', false)) { + return true; + } + $condition = $config->getValue('profiler.condition', []); + if (isset($condition['shared_secret'])) { + if ($request->getMethod() === 'PUT' && + !str_contains($request->getHeader('Content-Type'), 'application/x-www-form-urlencoded') && + !str_contains($request->getHeader('Content-Type'), 'application/json')) { + $logSecretRequest = ''; + } else { + $logSecretRequest = $request->getParam('profiler_secret', ''); + } + + // if token is found in the request change set the log condition to satisfied + if (hash_equals($condition['shared_secret'], $logSecretRequest)) { + return true; + } } + return false; } public function add(IDataCollector $dataCollector): void { @@ -76,7 +103,7 @@ public function saveProfile(IProfile $profile): bool { } } - public function collect(Request $request, Response $response): IProfile { + public function collect(IRequest $request, Response $response): IProfile { $profile = new Profile($request->getId()); $profile->setTime(time()); $profile->setUrl($request->getRequestUri()); @@ -94,13 +121,15 @@ public function collect(Request $request, Response $response): IProfile { /** * @return array[] */ - public function find(?string $url, ?int $limit, ?string $method, ?int $start, ?int $end, - string $statusCode = null): array { - if ($this->storage) { - return $this->storage->find($url, $limit, $method, $start, $end, $statusCode); - } else { - return []; - } + public function find( + ?string $url, + ?int $limit, + ?string $method, + ?int $start, + ?int $end, + string $statusCode = null + ): array { + return $this->storage->find($url, $limit, $method, $start, $end, $statusCode); } public function dataProviders(): array { @@ -113,6 +142,10 @@ public function isEnabled(): bool { public function setEnabled(bool $enabled): void { $this->enabled = $enabled; + if ($enabled) { + $this->add(new HttpDataCollector()); + $this->add(new MemoryDataCollector()); + } } public function clear(): void { diff --git a/lib/private/Server.php b/lib/private/Server.php index ba8b18f9a05bd..86111820e0962 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -61,6 +61,7 @@ use OC\AppFramework\Bootstrap\Coordinator; use OC\AppFramework\Http\Request; use OC\AppFramework\Http\RequestId; +use OC\AppFramework\Http\RequestVars; use OC\AppFramework\Utility\TimeFactory; use OC\Authentication\Events\LoginFailed; use OC\Authentication\Listeners\LoginFailedListener; @@ -351,9 +352,7 @@ public function __construct($webRoot, \OC\Config $config) { ); }); - $this->registerService(IProfiler::class, function (Server $c) { - return new Profiler($c->get(SystemConfig::class)); - }); + $this->registerAlias(IProfiler::class, Profiler::class); $this->registerService(\OCP\Encryption\IManager::class, function (Server $c): Encryption\Manager { $view = new View(); @@ -846,7 +845,7 @@ public function __construct($webRoot, \OC\Config $config) { }); $this->registerDeprecatedAlias('HttpClientService', IClientService::class); $this->registerService(IEventLogger::class, function (ContainerInterface $c) { - return new EventLogger($c->get(SystemConfig::class), $c->get(LoggerInterface::class), $c->get(Log::class)); + return new EventLogger($c->get(SystemConfig::class), $c->get(LoggerInterface::class), $c->get(Log::class), $c->get(IProfiler::class)); }); /** @deprecated 19.0.0 */ $this->registerDeprecatedAlias('EventLogger', IEventLogger::class); @@ -984,6 +983,27 @@ public function __construct($webRoot, \OC\Config $config) { $c->get(IMimeTypeDetector::class) ); }); + $this->registerService(RequestVars::class, function (ContainerInterface $c) { + if (isset($this['urlParams'])) { + $urlParams = $this['urlParams']; + } else { + $urlParams = []; + } + + return new RequestVars( + [ + 'get' => $_GET, + 'post' => $_POST, + 'files' => $_FILES, + 'server' => $_SERVER, + 'env' => $_ENV, + 'cookies' => $_COOKIE, + 'method' => (isset($_SERVER) && isset($_SERVER['REQUEST_METHOD'])) + ? $_SERVER['REQUEST_METHOD'] + : '', + 'urlParams' => $urlParams, + ]); + }); $this->registerService(\OCP\IRequest::class, function (ContainerInterface $c) { if (isset($this['urlParams'])) { $urlParams = $this['urlParams']; diff --git a/lib/public/Profiler/IProfiler.php b/lib/public/Profiler/IProfiler.php index 5fa4582add797..eb3479ebdd9aa 100644 --- a/lib/public/Profiler/IProfiler.php +++ b/lib/public/Profiler/IProfiler.php @@ -29,6 +29,7 @@ use OC\AppFramework\Http\Request; use OCP\AppFramework\Http\Response; use OCP\DataCollector\IDataCollector; +use OCP\IRequest; /** * This interface allows to interact with the built-in Nextcloud profiler. @@ -97,7 +98,7 @@ public function setEnabled(bool $enabled): void; * a IProfile from it. * @since 24.0.0 */ - public function collect(Request $request, Response $response): IProfile; + public function collect(IRequest $request, Response $response): IProfile; /** * Clear the stored profiles diff --git a/tests/lib/Diagnostics/EventLoggerTest.php b/tests/lib/Diagnostics/EventLoggerTest.php index 18cd3a91b1a1a..800f32eb23cf8 100644 --- a/tests/lib/Diagnostics/EventLoggerTest.php +++ b/tests/lib/Diagnostics/EventLoggerTest.php @@ -21,6 +21,7 @@ namespace Test\Diagnostics; +use OCP\Profiler\IProfiler; use Psr\Log\LoggerInterface; use OC\Diagnostics\EventLogger; use OC\Log; @@ -37,7 +38,8 @@ protected function setUp(): void { $this->logger = new EventLogger( $this->createMock(SystemConfig::class), $this->createMock(LoggerInterface::class), - $this->createMock(Log::class) + $this->createMock(Log::class), + $this->createMock(IProfiler::class), ); }