Skip to content

Commit 077fd94

Browse files
committed
Collect backtrace
1 parent e2afd97 commit 077fd94

File tree

7 files changed

+225
-148
lines changed

7 files changed

+225
-148
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"symfony/filesystem": "^6.3",
2828
"symfony/framework-bundle": "^6.3.5",
2929
"symfony/phpunit-bridge": "^6.3",
30+
"symfony/stopwatch": "^6.3",
3031
"symfony/web-profiler-bundle": "^6.3",
3132
"symfony/yaml": "^6.3",
3233
"zenstruck/browser": "^1.6"

config/services.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
use MongoDB\Bundle\Client;
2424
use MongoDB\Bundle\Command\DebugCommand;
25+
use MongoDB\Bundle\DataCollector\DriverEventSubscriber;
2526
use MongoDB\Bundle\DataCollector\MongoDBDataCollector;
2627

2728
return static function (ContainerConfigurator $container): void {
@@ -39,13 +40,22 @@
3940
->tag('console.command');
4041

4142
$services
42-
->set('mongodb.prototype.client', Client::class)
43+
->set('mongodb.abstract.client', Client::class)
4344
->arg('$uri', abstract_arg('Should be defined by pass'))
4445
->arg('$uriOptions', abstract_arg('Should be defined by pass'))
45-
->arg('$driverOptions', abstract_arg('Should be defined by pass'));
46+
->arg('$driverOptions', abstract_arg('Should be defined by pass'))
47+
->abstract();
48+
49+
$services
50+
->set('mongodb.abstract.driver_event_subscriber', DriverEventSubscriber::class)
51+
->arg('$clientName', abstract_arg('Should be defined by pass'))
52+
->arg('$dataCollector', service('mongodb.data_collector'))
53+
->arg('$stopwatch', service('debug.stopwatch')->nullOnInvalid())
54+
->abstract();
4655

4756
$services
4857
->set('mongodb.data_collector', MongoDBDataCollector::class)
58+
->arg('$clients', tagged_iterator('mongodb.client', 'name'))
4959
->tag('data_collector', [
5060
'template' => '@MongoDB/Collector/mongodb.html.twig',
5161
'id' => 'mongodb',

src/DataCollector/DriverEventSubscriber.php

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -25,65 +25,80 @@
2525
use MongoDB\Driver\Monitoring\CommandSubscriber;
2626
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
2727
use Symfony\Component\Stopwatch\Stopwatch;
28-
use Symfony\Contracts\Service\ResetInterface;
28+
use Symfony\Component\Stopwatch\StopwatchEvent;
29+
30+
use function debug_backtrace;
31+
32+
use const DEBUG_BACKTRACE_IGNORE_ARGS;
2933

3034
/** @internal */
31-
final class DriverEventSubscriber implements CommandSubscriber, ResetInterface
35+
final class DriverEventSubscriber implements CommandSubscriber
3236
{
33-
/**
34-
* @var list<CommandFailedEvent|CommandStartedEvent|CommandSucceededEvent>
35-
*/
36-
private array $events = [];
37+
/** @var array<string, StopwatchEvent> */
3738
private array $stopwatchEvents = [];
3839

3940
public function __construct(
40-
private string $clientName,
41-
private ?Stopwatch $stopwatch = null,
41+
private readonly string $clientName,
42+
private readonly MongoDBDataCollector $dataCollector,
43+
private readonly ?Stopwatch $stopwatch = null,
4244
) {
4345
}
4446

45-
/**
46-
* @return list<CommandFailedEvent|CommandStartedEvent|CommandSucceededEvent>
47-
*/
48-
public function getEvents(): array
47+
public function commandStarted(CommandStartedEvent $event): void
4948
{
50-
return $this->events;
51-
}
49+
$requestId = $event->getRequestId();
5250

53-
public function commandFailed(CommandFailedEvent $event): void
54-
{
55-
$this->events[] = $event;
51+
$command = (array) $event->getCommand();
52+
unset($command['lsid'], $command['$clusterTime']);
5653

57-
if (isset($this->stopwatchEvents[$event->getRequestId()])) {
58-
$this->stopwatchEvents[$event->getRequestId()]->stop();
59-
unset($this->stopwatchEvents[$event->getRequestId()]);
60-
}
54+
$this->dataCollector->collectCommandEvent($this->clientName, $requestId, [
55+
'clientName' => $this->clientName,
56+
'databaseName' => $event->getDatabaseName(),
57+
'commandName' => $event->getCommandName(),
58+
'command' => $command,
59+
'operationId' => $event->getOperationId(),
60+
'serviceId' => $event->getServiceId(),
61+
'backtrace' => $this->filterBacktrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)),
62+
]);
63+
64+
$this->stopwatchEvents[$requestId] = $this->stopwatch?->start(
65+
'mongodb.' . $this->clientName . '.' . $event->getCommandName(),
66+
'mongodb',
67+
);
6168
}
6269

63-
public function commandStarted(CommandStartedEvent $event): void
70+
public function commandSucceeded(CommandSucceededEvent $event): void
6471
{
65-
$this->events[] = $event;
66-
67-
if ($this->stopwatch) {
68-
$this->stopwatchEvents[$event->getRequestId()] = $this->stopwatch->start(
69-
'mongodb.'.$this->clientName.'.'.$event->getCommandName(),
70-
'mongodb',
71-
);
72-
}
72+
$requestId = $event->getRequestId();
73+
74+
$this->stopwatchEvents[$requestId]?->stop();
75+
unset($this->stopwatchEvents[$requestId]);
76+
77+
$this->dataCollector->collectCommandEvent($this->clientName, $requestId, [
78+
'clientName' => $this->clientName,
79+
'durationMicros' => $event->getDurationMicros(),
80+
]);
7381
}
7482

75-
public function commandSucceeded(CommandSucceededEvent $event): void
83+
public function commandFailed(CommandFailedEvent $event): void
7684
{
77-
$this->events[] = $event;
85+
$requestId = $event->getRequestId();
86+
87+
$this->stopwatchEvents[$requestId]?->stop();
88+
unset($this->stopwatchEvents[$requestId]);
7889

79-
if (isset($this->stopwatchEvents[$event->getRequestId()])) {
80-
$this->stopwatchEvents[$event->getRequestId()]->stop();
81-
unset($this->stopwatchEvents[$event->getRequestId()]);
82-
}
90+
$this->dataCollector->collectCommandEvent($this->clientName, $requestId, [
91+
'clientName' => $this->clientName,
92+
'durationMicros' => $event->getDurationMicros(),
93+
'error' => $event->getError(),
94+
]);
8395
}
8496

85-
public function reset(): void
97+
private function filterBacktrace(array $backtrace): array
8698
{
87-
$this->events = [];
99+
// skip first since it's always the current method
100+
array_shift($backtrace);
101+
102+
return $backtrace;
88103
}
89104
}

src/DataCollector/MongoDBDataCollector.php

Lines changed: 59 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -20,97 +20,88 @@
2020

2121
namespace MongoDB\Bundle\DataCollector;
2222

23-
use MongoDB\Bundle\Client;
24-
use MongoDB\Driver\Monitoring\CommandFailedEvent;
25-
use MongoDB\Driver\Monitoring\CommandStartedEvent;
26-
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
23+
use MongoDB\Client;
24+
use MongoDB\Driver\Command;
2725
use Symfony\Component\HttpFoundation\Request;
2826
use Symfony\Component\HttpFoundation\Response;
2927
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
28+
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
29+
use Throwable;
30+
31+
use function array_column;
32+
use function array_diff_key;
33+
use function array_map;
34+
use function array_sum;
35+
use function count;
36+
use function debug_backtrace;
37+
use function dump;
38+
use function iterator_to_array;
39+
40+
use const DEBUG_BACKTRACE_IGNORE_ARGS;
3041

3142
/** @internal */
32-
final class MongoDBDataCollector extends DataCollector
43+
final class MongoDBDataCollector extends DataCollector implements LateDataCollectorInterface
3344
{
3445
/**
35-
* @var list<array{client:Client, subscriber:DriverEventSubscriber}>
46+
* The list of request by client name is built with driver event data.
47+
*
48+
* @var array<string, array<string, array{clientName:string,databaseName:string,commandName:string,command:array,operationId:int,serviceId:int,durationMicros?:int,error?:string}>>
3649
*/
37-
private array $clients = [];
50+
private array $requests = [];
3851

39-
public function addClient(string $name, Client $client, DriverEventSubscriber $subscriber): void
40-
{
41-
$this->clients[$name] = [
42-
'client' => $client,
43-
'subscriber' => $subscriber,
44-
];
52+
public function __construct(
53+
/** @var iterable<string, Client> */
54+
private readonly iterable $clients = [],
55+
) {
4556
}
4657

47-
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
58+
public function collectCommandEvent(string $clientName, string $requestId, array $data): void
4859
{
49-
foreach ($this->clients as $name => ['client' => $client, 'subscriber' => $subscriber]) {
50-
$totalTime = 0;
51-
$requestCount = 0;
52-
$errorCount = 0;
53-
$requests = [];
54-
55-
foreach ($subscriber->getEvents() as $event) {
56-
$requestId = $event->getRequestId();
57-
58-
if ($event instanceof CommandStartedEvent) {
59-
$command = (array) $event->getCommand();
60-
unset($command['lsid'], $command['$clusterTime']);
61-
62-
$requests[$requestId] = [
63-
'client' => $name,
64-
'startedAt' => hrtime(true),
65-
'commandName' => $event->getCommandName(),
66-
'command' => $command,
67-
'operationId' => $event->getOperationId(),
68-
'database' => $event->getDatabaseName(),
69-
'serviceId' => $event->getServiceId(),
70-
];
71-
++$requestCount;
72-
} elseif ($event instanceof CommandSucceededEvent) {
73-
$requests[$requestId] += [
74-
'duration' => $event->getDurationMicros(),
75-
'endedAt' => hrtime(true),
76-
'success' => true,
77-
];
78-
$totalTime += $event->getDurationMicros();
79-
} elseif ($event instanceof CommandFailedEvent) {
80-
$requests[$requestId] += [
81-
'duration' => $event->getDurationMicros(),
82-
'error' => $event->getError(),
83-
'success' => false,
84-
];
85-
$totalTime += $event->getDurationMicros();
86-
++$errorCount;
87-
}
88-
}
89-
90-
$this->data['clients'][$name] = [
91-
'name' => $name,
92-
'uri' => (string) $client,
93-
'totalTime' => $totalTime,
94-
'requestCount' => $requestCount,
95-
'errorCount' => $errorCount,
96-
'requests' => $requests,
97-
];
60+
if (isset($this->requests[$clientName][$requestId])) {
61+
$this->requests[$clientName][$requestId] += $data;
62+
} else {
63+
$this->requests[$clientName][$requestId] = $data;
9864
}
9965
}
10066

67+
public function collect(Request $request, Response $response, ?Throwable $exception = null): void
68+
{
69+
}
70+
71+
public function lateCollect(): void
72+
{
73+
$this->data = [
74+
'clients' => array_map(static fn (Client $client) => [
75+
'serverBuildInfo' => $client->getManager()->executeCommand('admin', new Command(['buildInfo' => 1]))->toArray()[0],
76+
'clientInfo' => array_diff_key($client->__debugInfo(), ['manager' => 1]),
77+
], iterator_to_array($this->clients)),
78+
'requests' => $this->requests,
79+
'requestCount' => array_sum(array_map(count(...), $this->requests)),
80+
'errorCount' => array_sum(array_map(static fn (array $requests) => count(array_column($requests, 'error')), $this->requests)),
81+
'durationMicros' => array_sum(array_map(static fn (array $requests) => array_sum(array_column($requests, 'durationMicros')), $this->requests)),
82+
];
83+
84+
dump($this->data, array_column(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), 'class'));
85+
}
86+
10187
public function getRequestCount(): int
10288
{
103-
return array_sum(array_column($this->data['clients'], 'requestCount'));
89+
return $this->data['requestCount'];
10490
}
10591

10692
public function getErrorCount(): int
10793
{
108-
return array_sum(array_column($this->data['clients'], 'errorCount'));
94+
return $this->data['errorCount'];
95+
}
96+
97+
public function getTime(): int
98+
{
99+
return $this->data['durationMicros'];
109100
}
110101

111-
public function getTime(): float
102+
public function getRequests(): array
112103
{
113-
return array_sum(array_column($this->data['clients'], 'totalTime'));
104+
return $this->data['requests'];
114105
}
115106

116107
public function getClients(): array
@@ -125,10 +116,7 @@ public function getName(): string
125116

126117
public function reset(): void
127118
{
119+
$this->requests = [];
128120
$this->data = [];
129-
130-
foreach ($this->clients as ['subscriber' => $subscriber]) {
131-
$subscriber->reset();
132-
}
133121
}
134122
}

src/DependencyInjection/Compiler/DataCollectorPass.php

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,39 +21,31 @@
2121
namespace MongoDB\Bundle\DependencyInjection\Compiler;
2222

2323
use MongoDB\Bundle\DataCollector\DriverEventSubscriber;
24+
use Symfony\Component\DependencyInjection\ChildDefinition;
2425
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
2526
use Symfony\Component\DependencyInjection\ContainerBuilder;
2627
use Symfony\Component\DependencyInjection\ContainerInterface;
2728
use Symfony\Component\DependencyInjection\Definition;
2829
use Symfony\Component\DependencyInjection\Reference;
2930

31+
use function sprintf;
32+
3033
/** @internal */
3134
final class DataCollectorPass implements CompilerPassInterface
3235
{
3336
public function process(ContainerBuilder $container): void
3437
{
35-
if (!$container->has('profiler')) {
38+
if (! $container->has('profiler')) {
3639
return;
3740
}
3841

39-
$dataCollector = $container->getDefinition('mongodb.data_collector');
40-
4142
// Add a subscriber to each client to collect driver events, and register the client to the data collector.
4243
foreach ($container->findTaggedServiceIds('mongodb.client', true) as $clientId => $attributes) {
4344
$subscriberId = sprintf('%s.subscriber', $clientId);
44-
$subscriber = new Definition(DriverEventSubscriber::class);
45-
$subscriber->setArguments([
46-
$attributes[0]['name'] ?? $clientId,
47-
new Reference('debug.stopwatch', ContainerInterface::NULL_ON_INVALID_REFERENCE),
48-
]);
45+
$subscriber = new ChildDefinition('mongodb.abstract.driver_event_subscriber');
46+
$subscriber->replaceArgument('$clientName', $attributes[0]['name'] ?? $clientId);
4947
$container->setDefinition($subscriberId, $subscriber);
50-
5148
$container->getDefinition($clientId)->addMethodCall('addSubscriber', [new Reference($subscriberId)]);
52-
$dataCollector->addMethodCall('addClient', [
53-
$attributes[0]['name'] ?? $clientId,
54-
new Reference($clientId),
55-
new Reference($subscriberId),
56-
]);
5749
}
5850
}
5951
}

0 commit comments

Comments
 (0)