Browse Source
feat(dav): report inefficient DAV plugins in logs
feat(dav): report inefficient DAV plugins in logs
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>pull/54153/head
7 changed files with 329 additions and 3 deletions
-
1apps/dav/composer/composer/autoload_classmap.php
-
1apps/dav/composer/composer/autoload_static.php
-
78apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php
-
112apps/dav/lib/Connector/Sabre/Server.php
-
10apps/dav/lib/Connector/Sabre/ServerFactory.php
-
7apps/dav/lib/Server.php
-
123apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php
@ -0,0 +1,78 @@ |
|||
<?php |
|||
|
|||
declare(strict_types = 1); |
|||
|
|||
/** |
|||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors |
|||
* SPDX-License-Identifier: AGPL-3.0-or-later |
|||
*/ |
|||
namespace OCA\DAV\Connector\Sabre; |
|||
|
|||
use Sabre\DAV\Server as SabreServer; |
|||
use Sabre\DAV\ServerPlugin; |
|||
use Sabre\HTTP\RequestInterface; |
|||
use Sabre\HTTP\ResponseInterface; |
|||
|
|||
/** |
|||
* This plugin runs after requests and logs an error if a plugin is detected |
|||
* to be doing too many SQL requests. |
|||
*/ |
|||
class PropFindMonitorPlugin extends ServerPlugin { |
|||
|
|||
/** |
|||
* A Plugin can scan up to this amount of nodes without an error being |
|||
* reported. |
|||
*/ |
|||
public const THRESHOLD_NODES = 50; |
|||
|
|||
/** |
|||
* A plugin can use up to this amount of queries per node. |
|||
*/ |
|||
public const THRESHOLD_QUERY_FACTOR = 1; |
|||
|
|||
private SabreServer $server; |
|||
|
|||
public function initialize(SabreServer $server): void { |
|||
$this->server = $server; |
|||
$this->server->on('afterResponse', [$this, 'afterResponse']); |
|||
} |
|||
|
|||
public function afterResponse( |
|||
RequestInterface $request, |
|||
ResponseInterface $response): void { |
|||
if (!$this->server instanceof Server) { |
|||
return; |
|||
} |
|||
|
|||
$pluginQueries = $this->server->getPluginQueries(); |
|||
if (empty($pluginQueries)) { |
|||
return; |
|||
} |
|||
$maxDepth = max(0, ...array_keys($pluginQueries)); |
|||
// entries at the top are usually not interesting
|
|||
unset($pluginQueries[$maxDepth]); |
|||
|
|||
$logger = $this->server->getLogger(); |
|||
foreach ($pluginQueries as $depth => $propFinds) { |
|||
foreach ($propFinds as $pluginName => $propFind) { |
|||
[ |
|||
'queries' => $queries, |
|||
'nodes' => $nodes |
|||
] = $propFind; |
|||
if ($queries === 0 || $nodes > $queries || $nodes < self::THRESHOLD_NODES |
|||
|| $queries < $nodes * self::THRESHOLD_QUERY_FACTOR) { |
|||
continue; |
|||
} |
|||
$logger->error( |
|||
'{name} scanned {scans} nodes with {count} queries in depth {depth}/{maxDepth}. This is bad for performance, please report to the plugin developer!', [ |
|||
'name' => $pluginName, |
|||
'scans' => $nodes, |
|||
'count' => $queries, |
|||
'depth' => $depth, |
|||
'maxDepth' => $maxDepth, |
|||
] |
|||
); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,123 @@ |
|||
<?php |
|||
|
|||
/** |
|||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors |
|||
* SPDX-License-Identifier: AGPL-3.0-or-later |
|||
*/ |
|||
namespace unit\Connector\Sabre; |
|||
|
|||
use OCA\DAV\Connector\Sabre\PropFindMonitorPlugin; |
|||
use OCA\DAV\Connector\Sabre\Server; |
|||
use PHPUnit\Framework\MockObject\MockObject; |
|||
use Psr\Log\LoggerInterface; |
|||
use Sabre\HTTP\Request; |
|||
use Sabre\HTTP\Response; |
|||
use Test\TestCase; |
|||
|
|||
class PropFindMonitorPluginTest extends TestCase { |
|||
|
|||
private PropFindMonitorPlugin $plugin; |
|||
private Server&MockObject $server; |
|||
private LoggerInterface&MockObject $logger; |
|||
private Request&MockObject $request; |
|||
private Response&MockObject $response; |
|||
|
|||
public static function dataTest(): array { |
|||
$minQueriesTrigger = PropFindMonitorPlugin::THRESHOLD_QUERY_FACTOR |
|||
* PropFindMonitorPlugin::THRESHOLD_NODES; |
|||
return [ |
|||
'No queries logged' => [[], 0], |
|||
'Plugins with queries in less than threshold nodes should not be logged' => [ |
|||
[ |
|||
[ |
|||
'PluginName' => ['queries' => 100, 'nodes' |
|||
=> PropFindMonitorPlugin::THRESHOLD_NODES - 1] |
|||
], |
|||
[], |
|||
], |
|||
0 |
|||
], |
|||
'Plugins with query-to-node ratio less than threshold should not be logged' => [ |
|||
[ |
|||
[ |
|||
'PluginName' => [ |
|||
'queries' => $minQueriesTrigger - 1, |
|||
'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES ], |
|||
], |
|||
[], |
|||
], |
|||
0 |
|||
], |
|||
'Plugins with more nodes scanned than queries executed should not be logged' => [ |
|||
[ |
|||
[ |
|||
'PluginName' => [ |
|||
'queries' => $minQueriesTrigger, |
|||
'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES * 2], |
|||
], |
|||
[], |
|||
], |
|||
0 |
|||
], |
|||
'Plugins with queries only in highest depth level should not be logged' => [ |
|||
[ |
|||
[ |
|||
'PluginName' => [ |
|||
'queries' => $minQueriesTrigger, |
|||
'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES - 1 |
|||
] |
|||
], |
|||
[ |
|||
'PluginName' => [ |
|||
'queries' => $minQueriesTrigger * 2, |
|||
'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES |
|||
] |
|||
] |
|||
], |
|||
0 |
|||
], |
|||
'Plugins with too many queries should be logged' => [ |
|||
[ |
|||
[ |
|||
'FirstPlugin' => [ |
|||
'queries' => $minQueriesTrigger, |
|||
'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES, |
|||
], |
|||
'SecondPlugin' => [ |
|||
'queries' => $minQueriesTrigger, |
|||
'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES, |
|||
] |
|||
], |
|||
[] |
|||
], |
|||
2 |
|||
] |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider dataTest |
|||
*/ |
|||
public function test(array $queries, $expectedLogCalls): void { |
|||
$this->plugin->initialize($this->server); |
|||
$this->server->expects($this->once())->method('getPluginQueries') |
|||
->willReturn($queries); |
|||
|
|||
$this->server->expects(empty($queries) ? $this->never() : $this->once()) |
|||
->method('getLogger') |
|||
->willReturn($this->logger); |
|||
|
|||
$this->logger->expects($this->exactly($expectedLogCalls))->method('error'); |
|||
$this->plugin->afterResponse($this->request, $this->response); |
|||
} |
|||
|
|||
protected function setUp(): void { |
|||
parent::setUp(); |
|||
|
|||
$this->plugin = new PropFindMonitorPlugin(); |
|||
$this->server = $this->createMock(Server::class); |
|||
$this->logger = $this->createMock(LoggerInterface::class); |
|||
$this->request = $this->createMock(Request::class); |
|||
$this->response = $this->createMock(Response::class); |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue