-
Notifications
You must be signed in to change notification settings - Fork 3
Add MongoDB profiler #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
fbfbfa1
Init profiler
GromNaN 6655af0
Use a compiler pass to register data collector conditionally
GromNaN c61cdad
Add tests on data collector services
GromNaN ade9e87
Implement DataCollector::reset()
GromNaN af0a049
Rework MongoDB logo
GromNaN 3a453fa
Add stopwatch
GromNaN e54a8f5
Collect backtrace
GromNaN 47ad991
Pretty render backtrace
GromNaN 08aa322
Fix tests and CS
GromNaN e07a167
Add client info
GromNaN ecf0063
Simplify DI using configurator
GromNaN f9e288c
Add tests to collector
GromNaN 3ec628a
Fix missing icon and inline stub class
GromNaN 821bc1e
Skip trace lines without a file
alcaeus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace MongoDB\Bundle\DataCollector; | ||
|
||
/** @internal */ | ||
interface CommandEventCollector | ||
{ | ||
public function collectCommandEvent(int $clientId, string $requestId, array $data): void; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/** | ||
* Copyright 2023-present MongoDB, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
namespace MongoDB\Bundle\DataCollector; | ||
|
||
use MongoDB\Driver\Monitoring\CommandFailedEvent; | ||
use MongoDB\Driver\Monitoring\CommandStartedEvent; | ||
use MongoDB\Driver\Monitoring\CommandSubscriber; | ||
use MongoDB\Driver\Monitoring\CommandSucceededEvent; | ||
use Symfony\Component\Stopwatch\Stopwatch; | ||
use Symfony\Component\Stopwatch\StopwatchEvent; | ||
|
||
use function array_shift; | ||
use function debug_backtrace; | ||
|
||
use const DEBUG_BACKTRACE_IGNORE_ARGS; | ||
|
||
/** @internal */ | ||
final class DriverEventSubscriber implements CommandSubscriber | ||
{ | ||
/** @var array<string, StopwatchEvent> */ | ||
private array $stopwatchEvents = []; | ||
|
||
public function __construct( | ||
private readonly int $clientId, | ||
private readonly CommandEventCollector $collector, | ||
private readonly ?Stopwatch $stopwatch = null, | ||
) { | ||
} | ||
|
||
public function commandStarted(CommandStartedEvent $event): void | ||
{ | ||
$requestId = $event->getRequestId(); | ||
|
||
$command = (array) $event->getCommand(); | ||
unset($command['lsid'], $command['$clusterTime']); | ||
|
||
$data = [ | ||
'databaseName' => $event->getDatabaseName(), | ||
'commandName' => $event->getCommandName(), | ||
'command' => $command, | ||
'operationId' => $event->getOperationId(), | ||
'serviceId' => $event->getServiceId(), | ||
'backtrace' => $this->filterBacktrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)), | ||
]; | ||
|
||
if ($event->getCommandName() === 'getMore') { | ||
$data['cursorId'] = $event->getCommand()->getMore; | ||
} | ||
|
||
$this->collector->collectCommandEvent($this->clientId, $requestId, $data); | ||
|
||
$this->stopwatchEvents[$requestId] = $this->stopwatch?->start( | ||
'mongodb.' . $event->getCommandName(), | ||
'mongodb', | ||
); | ||
} | ||
|
||
public function commandSucceeded(CommandSucceededEvent $event): void | ||
{ | ||
$requestId = $event->getRequestId(); | ||
|
||
$this->stopwatchEvents[$requestId]?->stop(); | ||
unset($this->stopwatchEvents[$requestId]); | ||
|
||
$data = [ | ||
'durationMicros' => $event->getDurationMicros(), | ||
]; | ||
|
||
if (isset($event->getReply()->cursor)) { | ||
$data['cursorId'] = $event->getReply()->cursor->id; | ||
} | ||
|
||
$this->collector->collectCommandEvent($this->clientId, $requestId, $data); | ||
} | ||
|
||
public function commandFailed(CommandFailedEvent $event): void | ||
{ | ||
$requestId = $event->getRequestId(); | ||
|
||
$this->stopwatchEvents[$requestId]?->stop(); | ||
unset($this->stopwatchEvents[$requestId]); | ||
|
||
$data = [ | ||
'durationMicros' => $event->getDurationMicros(), | ||
'error' => (string) $event->getError(), | ||
]; | ||
|
||
$this->collector->collectCommandEvent($this->clientId, $requestId, $data); | ||
} | ||
|
||
private function filterBacktrace(array $backtrace): array | ||
{ | ||
// skip first since it's always the current method | ||
array_shift($backtrace); | ||
|
||
return $backtrace; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/** | ||
* Copyright 2023-present MongoDB, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
namespace MongoDB\Bundle\DataCollector; | ||
|
||
use LogicException; | ||
use MongoDB\Client; | ||
use MongoDB\Driver\Command; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpFoundation\Response; | ||
use Symfony\Component\HttpKernel\DataCollector\DataCollector; | ||
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; | ||
use Symfony\Component\Stopwatch\Stopwatch; | ||
use Throwable; | ||
|
||
use function array_diff_key; | ||
use function spl_object_id; | ||
|
||
/** @internal */ | ||
final class MongoDBDataCollector extends DataCollector implements LateDataCollectorInterface, CommandEventCollector | ||
{ | ||
/** | ||
* The list of request by client ID is built with driver event data. | ||
* | ||
* @var array<string, array<string, array{clientName:string,databaseName:string,commandName:string,command:array,operationId:int,serviceId:int,durationMicros?:int,error?:string}>> | ||
*/ | ||
private array $requests = []; | ||
|
||
public function __construct( | ||
private readonly ?Stopwatch $stopwatch = null, | ||
/** @var iterable<string, Client> */ | ||
private readonly iterable $clients = [], | ||
) { | ||
} | ||
|
||
public function configureClient(Client $client): void | ||
{ | ||
$client->getManager()->addSubscriber(new DriverEventSubscriber(spl_object_id($client), $this, $this->stopwatch)); | ||
} | ||
|
||
public function collectCommandEvent(int $clientId, string $requestId, array $data): void | ||
{ | ||
if (isset($this->requests[$clientId][$requestId])) { | ||
$this->requests[$clientId][$requestId] += $data; | ||
} else { | ||
$this->requests[$clientId][$requestId] = $data; | ||
} | ||
} | ||
|
||
public function collect(Request $request, Response $response, ?Throwable $exception = null): void | ||
{ | ||
} | ||
|
||
public function lateCollect(): void | ||
{ | ||
$requestCount = 0; | ||
$errorCount = 0; | ||
$durationMicros = 0; | ||
|
||
$clients = []; | ||
$clientIdMap = []; | ||
foreach ($this->clients as $name => $client) { | ||
$clientIdMap[spl_object_id($client)] = $name; | ||
$clients[$name] = [ | ||
'serverBuildInfo' => array_diff_key( | ||
(array) $client->getManager()->executeCommand('admin', new Command(['buildInfo' => 1]))->toArray()[0], | ||
['versionArray' => 0, 'ok' => 0], | ||
), | ||
'clientInfo' => array_diff_key($client->__debugInfo(), ['manager' => 0]), | ||
]; | ||
} | ||
|
||
$requests = []; | ||
foreach ($this->requests as $clientId => $requestsByClientId) { | ||
$clientName = $clientIdMap[$clientId] ?? throw new LogicException('Client not found'); | ||
foreach ($requestsByClientId as $requestId => $request) { | ||
$requests[$clientName][$requestId] = $request; | ||
$requestCount++; | ||
$durationMicros += $request['durationMicros'] ?? 0; | ||
$errorCount += isset($request['error']) ? 1 : 0; | ||
} | ||
} | ||
|
||
$this->data = [ | ||
'clients' => $clients, | ||
'requests' => $requests, | ||
'requestCount' => $requestCount, | ||
'errorCount' => $errorCount, | ||
'durationMicros' => $durationMicros, | ||
]; | ||
} | ||
|
||
public function getRequestCount(): int | ||
{ | ||
return $this->data['requestCount']; | ||
} | ||
|
||
public function getErrorCount(): int | ||
{ | ||
return $this->data['errorCount']; | ||
} | ||
|
||
public function getTime(): int | ||
{ | ||
return $this->data['durationMicros']; | ||
} | ||
|
||
public function getRequests(): array | ||
{ | ||
return $this->data['requests']; | ||
} | ||
|
||
public function getClients(): array | ||
{ | ||
return $this->data['clients']; | ||
} | ||
|
||
public function getName(): string | ||
{ | ||
return 'mongodb'; | ||
} | ||
|
||
public function reset(): void | ||
{ | ||
$this->requests = []; | ||
$this->data = []; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/** | ||
* Copyright 2023-present MongoDB, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
namespace MongoDB\Bundle\DependencyInjection\Compiler; | ||
|
||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; | ||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
use Symfony\Component\DependencyInjection\Reference; | ||
|
||
/** @internal */ | ||
final class DataCollectorPass implements CompilerPassInterface | ||
{ | ||
public function process(ContainerBuilder $container): void | ||
{ | ||
if (! $container->has('profiler')) { | ||
return; | ||
} | ||
|
||
/** | ||
* Add a subscriber to each client to collect driver events. | ||
* | ||
* @see \MongoDB\Bundle\DataCollector\MongoDBDataCollector::configureClient() | ||
*/ | ||
foreach ($container->findTaggedServiceIds('mongodb.client', true) as $clientId => $attributes) { | ||
$container->getDefinition($clientId)->setConfigurator([new Reference('mongodb.data_collector'), 'configureClient']); | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.