Skip to content

Commit 0325dbe

Browse files
committed
Add tests to collector
1 parent 1cb0e8b commit 0325dbe

File tree

9 files changed

+260
-22
lines changed

9 files changed

+260
-22
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MongoDB\Bundle\DataCollector;
6+
7+
/** @internal */
8+
interface CommandEventCollector
9+
{
10+
public function collectCommandEvent(int $clientId, string $requestId, array $data): void;
11+
}

src/DataCollector/DriverEventSubscriber.php

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ final class DriverEventSubscriber implements CommandSubscriber
4040

4141
public function __construct(
4242
private readonly int $clientId,
43-
private readonly MongoDBDataCollector $dataCollector,
43+
private readonly CommandEventCollector $collector,
4444
private readonly ?Stopwatch $stopwatch = null,
4545
) {
4646
}
@@ -52,14 +52,20 @@ public function commandStarted(CommandStartedEvent $event): void
5252
$command = (array) $event->getCommand();
5353
unset($command['lsid'], $command['$clusterTime']);
5454

55-
$this->dataCollector->collectCommandEvent($this->clientId, $requestId, [
55+
$data = [
5656
'databaseName' => $event->getDatabaseName(),
5757
'commandName' => $event->getCommandName(),
5858
'command' => $command,
5959
'operationId' => $event->getOperationId(),
6060
'serviceId' => $event->getServiceId(),
6161
'backtrace' => $this->filterBacktrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)),
62-
]);
62+
];
63+
64+
if ($event->getCommandName() === 'getMore') {
65+
$data['cursorId'] = $event->getCommand()->getMore;
66+
}
67+
68+
$this->collector->collectCommandEvent($this->clientId, $requestId, $data);
6369

6470
$this->stopwatchEvents[$requestId] = $this->stopwatch?->start(
6571
'mongodb.' . $event->getCommandName(),
@@ -74,9 +80,15 @@ public function commandSucceeded(CommandSucceededEvent $event): void
7480
$this->stopwatchEvents[$requestId]?->stop();
7581
unset($this->stopwatchEvents[$requestId]);
7682

77-
$this->dataCollector->collectCommandEvent($this->clientId, $requestId, [
83+
$data = [
7884
'durationMicros' => $event->getDurationMicros(),
79-
]);
85+
];
86+
87+
if (isset($event->getReply()->cursor)) {
88+
$data['cursorId'] = $event->getReply()->cursor->id;
89+
}
90+
91+
$this->collector->collectCommandEvent($this->clientId, $requestId, $data);
8092
}
8193

8294
public function commandFailed(CommandFailedEvent $event): void
@@ -86,10 +98,12 @@ public function commandFailed(CommandFailedEvent $event): void
8698
$this->stopwatchEvents[$requestId]?->stop();
8799
unset($this->stopwatchEvents[$requestId]);
88100

89-
$this->dataCollector->collectCommandEvent($this->clientId, $requestId, [
101+
$data = [
90102
'durationMicros' => $event->getDurationMicros(),
91-
'error' => $event->getError(),
92-
]);
103+
'error' => (string) $event->getError(),
104+
];
105+
106+
$this->collector->collectCommandEvent($this->clientId, $requestId, $data);
93107
}
94108

95109
private function filterBacktrace(array $backtrace): array

src/DataCollector/MongoDBDataCollector.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@
3434
use function spl_object_id;
3535

3636
/** @internal */
37-
final class MongoDBDataCollector extends DataCollector implements LateDataCollectorInterface
37+
final class MongoDBDataCollector extends DataCollector implements LateDataCollectorInterface, CommandEventCollector
3838
{
3939
/**
40-
* The list of request by client name is built with driver event data.
40+
* The list of request by client ID is built with driver event data.
4141
*
4242
* @var array<string, array<string, array{clientName:string,databaseName:string,commandName:string,command:array,operationId:int,serviceId:int,durationMicros?:int,error?:string}>>
4343
*/

src/DependencyInjection/Compiler/DataCollectorPass.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ public function process(ContainerBuilder $container): void
3333
return;
3434
}
3535

36-
// Add a subscriber to each client to collect driver events, and register the client to the data collector.
36+
/**
37+
* Add a subscriber to each client to collect driver events.
38+
*
39+
* @see \MongoDB\Bundle\DataCollector\MongoDBDataCollector::configureClient()
40+
*/
3741
foreach ($container->findTaggedServiceIds('mongodb.client', true) as $clientId => $attributes) {
3842
$container->getDefinition($clientId)->setConfigurator([new Reference('mongodb.data_collector'), 'configureClient']);
3943
}

templates/Collector/mongodb.html.twig

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,10 @@
66
{{ parent() }}
77

88
<style>
9-
109
#collector-content .sf-toggle { cursor: pointer; position: relative; }
1110
#collector-content .sf-toggle-off .icon-close, .sf-toggle-on .icon-open { display: none; }
1211
#collector-content .sf-toggle-off .icon-open, .sf-toggle-on .icon-close { display: block; }
13-
14-
#collector-content .trace-details { background: var(--base-0); border: var(--border); box-shadow: 0px 0px 1px rgba(128, 128, 128, .2); margin: 0 0 1em; table-layout: fixed; }
15-
16-
#collector-content .trace-line .icon { opacity: .4; position: absolute; left: 10px; }
17-
#collector-content .trace-line .icon svg { fill: var(--base-5); height: 16px; width: 16px; }
12+
#collector-content .trace-details { background: var(--base-0); border: var(--border); box-shadow: 0 0 1px rgba(128, 128, 128, .2); margin: 0 0 1em; table-layout: fixed; }
1813
#collector-content .trace { background: var(--base-0); padding: 10px; margin: 0.5em 0; overflow: auto; font-size: 12px; }
1914
#collector-content .trace-method { color: var(--highlight-keyword); font-weight: bold; }
2015
#collector-content .trace li { margin-bottom: 0; padding: 0; }
@@ -26,7 +21,6 @@
2621
#collector-content .trace-line a { color: var(--base-6); }
2722
#collector-content .trace-line .icon { opacity: .4; position: absolute; left: 10px; }
2823
#collector-content .trace-line .icon svg { fill: var(--base-5); height: 16px; width: 16px; }
29-
#collector-content .trace-line .icon.icon-copy { left: auto; top: auto; padding-left: 5px; display: none }
3024
#collector-content .trace-line-header { padding-left: 36px; padding-right: 10px; }
3125
</style>
3226
{% endblock %}
@@ -139,15 +133,15 @@
139133
{{ dump(request.command) }}
140134

141135
<div class="text-small font-normal">
142-
<a href="#" class="sf-toggle link-inverse" data-toggle-selector="#formatted-request-{{ i }}-{{ loop.parent.loop.index }}" data-toggle-alt-content="Hide formatted request">View formatted request</a>
136+
<a href="#" class="sf-toggle link-inverse" data-toggle-selector="#formatted-command-{{ i }}-{{ loop.parent.loop.index }}" data-toggle-alt-content="Hide formatted command">View formatted command</a>
143137

144138
{% if request.backtrace is defined %}
145139
&nbsp;&nbsp;
146-
<a href="#" class="sf-toggle link-inverse" data-toggle-selector="#backtrace-{{ i }}-{{ loop.parent.loop.index }}" data-toggle-alt-content="Hide request backtrace">View request backtrace</a>
140+
<a href="#" class="sf-toggle link-inverse" data-toggle-selector="#backtrace-{{ i }}-{{ loop.parent.loop.index }}" data-toggle-alt-content="Hide command backtrace">View command backtrace</a>
147141
{% endif %}
148142
</div>
149143

150-
<div id="formatted-request-{{ i }}-{{ loop.parent.loop.index }}" class="sql-runnable hidden">
144+
<div id="formatted-command-{{ i }}-{{ loop.parent.loop.index }}" class="sql-runnable hidden">
151145
<pre style="white-space: pre-wrap">{{ request.command|json_encode(constant('JSON_PRETTY_PRINT')) }}</pre>
152146
<button class="btn btn-sm label hidden" data-clipboard-text="{{ request.command|json_encode|e('html_attr') }}">Copy</button>
153147
</div>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MongoDB\Bundle\Tests\Functional\DataCollector;
6+
7+
use MongoDB\Bundle\DataCollector\CommandEventCollector;
8+
use MongoDB\Bundle\DataCollector\DriverEventSubscriber;
9+
use MongoDB\Client;
10+
use MongoDB\Driver\Command;
11+
use MongoDB\Driver\Exception\ServerException;
12+
use PHPUnit\Framework\TestCase;
13+
use Symfony\Component\Stopwatch\Stopwatch;
14+
15+
class DriverEventSubscriberTest extends TestCase
16+
{
17+
private const CLIENT_ID = 123;
18+
19+
private CommandEventCollector $collector;
20+
private Stopwatch $stopwatch;
21+
22+
public function setUp(): void
23+
{
24+
$this->collector = new Stubs\CommandEventCollectorStub();
25+
$this->stopwatch = new Stopwatch();
26+
}
27+
28+
public function testCommandSucceeded(): void
29+
{
30+
$this->getClient()->selectCollection('database1', 'collection1')->find();
31+
32+
// The 2 events are commandStarted and commandSucceeded
33+
$this->assertCount(2, $this->collector->events);
34+
35+
// ClientId
36+
$this->assertSame(self::CLIENT_ID, $this->collector->events[0]['clientId']);
37+
$this->assertSame(self::CLIENT_ID, $this->collector->events[1]['clientId']);
38+
39+
// RequestId
40+
$this->assertSame($this->collector->events[0]['requestId'], $this->collector->events[1]['requestId'], 'Same $requestId');
41+
42+
// Data 1st event
43+
$this->assertSame('database1', $this->collector->events[0]['data']['databaseName']);
44+
$this->assertSame('find', $this->collector->events[0]['data']['commandName']);
45+
$this->assertArrayHasKey('command', $this->collector->events[0]['data']);
46+
$this->assertArrayHasKey('backtrace', $this->collector->events[0]['data']);
47+
48+
// Data 2nd event
49+
$this->assertArrayHasKey('durationMicros', $this->collector->events[1]['data']);
50+
$this->assertArrayNotHasKey('backtrace', $this->collector->events[1]['data']);
51+
}
52+
53+
public function testCommandFailed(): void
54+
{
55+
try {
56+
$this->getClient()->getManager()->executeCommand('database1', new Command(['invalid' => 'command']));
57+
$this->fail('Expected exception to be thrown');
58+
} catch (ServerException $e) {
59+
$message = $e->getMessage();
60+
}
61+
62+
// The 2 events are commandStarted and commandFailed
63+
$this->assertCount(2, $this->collector->events);
64+
65+
// ClientId
66+
$this->assertSame(self::CLIENT_ID, $this->collector->events[0]['clientId']);
67+
$this->assertSame(self::CLIENT_ID, $this->collector->events[1]['clientId']);
68+
69+
// RequestId
70+
$this->assertSame($this->collector->events[0]['requestId'], $this->collector->events[1]['requestId'], 'Same $requestId');
71+
72+
// Data 1st event
73+
$this->assertSame('database1', $this->collector->events[0]['data']['databaseName']);
74+
$this->assertSame('invalid', $this->collector->events[0]['data']['commandName']);
75+
$this->assertArrayHasKey('command', $this->collector->events[0]['data']);
76+
$this->assertArrayHasKey('backtrace', $this->collector->events[0]['data']);
77+
78+
// Data 2nd event
79+
$this->assertArrayHasKey('durationMicros', $this->collector->events[1]['data']);
80+
$this->assertArrayHasKey('error', $this->collector->events[1]['data']);
81+
$this->assertStringContainsString($message, $this->collector->events[1]['data']['error']);
82+
$this->assertArrayNotHasKey('backtrace', $this->collector->events[1]['data']);
83+
}
84+
85+
public function getClient(): Client
86+
{
87+
$subscriber = new DriverEventSubscriber(self::CLIENT_ID, $this->collector, $this->stopwatch);
88+
89+
$client = new Client($_SERVER['MONGODB_PRIMARY_URL'] ?? 'mongodb://localhost:27017');
90+
$client->getManager()->addSubscriber($subscriber);
91+
92+
return $client;
93+
}
94+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MongoDB\Bundle\Tests\Functional\DataCollector;
6+
7+
use ArrayIterator;
8+
use MongoDB\Bundle\DataCollector\MongoDBDataCollector;
9+
use MongoDB\Client;
10+
use PHPUnit\Framework\TestCase;
11+
use Symfony\Component\Stopwatch\Stopwatch;
12+
13+
use function array_keys;
14+
use function serialize;
15+
use function spl_object_id;
16+
use function unserialize;
17+
18+
class MongoDBDataCollectorTest extends TestCase
19+
{
20+
public function testCollectMultipleClients(): void
21+
{
22+
$client1 = new Client($_SERVER['MONGODB_PRIMARY_URL']);
23+
$client2 = new Client($_SERVER['MONGODB_PRIMARY_URL']);
24+
$stopwatch = new Stopwatch();
25+
26+
$dataCollector = new MongoDBDataCollector($stopwatch, new ArrayIterator(['client1' => $client1, 'client2' => $client2]));
27+
28+
$client1Id = spl_object_id($client1);
29+
$client2Id = spl_object_id($client2);
30+
31+
// Successful command on Client 1
32+
$dataCollector->collectCommandEvent($client1Id, 'request1', [
33+
'clientName' => 'client1',
34+
'databaseName' => 'database1',
35+
'commandName' => 'find',
36+
'command' => ['find' => 'collection1', 'filter' => []],
37+
]);
38+
$dataCollector->collectCommandEvent($client1Id, 'request1', ['durationMicros' => 1000]);
39+
40+
// Error on Client 1
41+
$dataCollector->collectCommandEvent($client1Id, 'request2', [
42+
'clientName' => 'client1',
43+
'databaseName' => 'database1',
44+
'commandName' => 'insert',
45+
'command' => ['insert' => 'collection1', 'documents' => []],
46+
]);
47+
$dataCollector->collectCommandEvent($client1Id, 'request2', [
48+
'durationMicros' => 500,
49+
'error' => 'Error message',
50+
]);
51+
52+
// Successful command on Client 2
53+
$dataCollector->collectCommandEvent($client2Id, 'request3', [
54+
'clientName' => 'client2',
55+
'databaseName' => 'database2',
56+
'commandName' => 'aggregate',
57+
'command' => ['aggregate' => 'collection2', 'pipeline' => []],
58+
]);
59+
$dataCollector->collectCommandEvent($client2Id, 'request3', ['durationMicros' => 800]);
60+
61+
$dataCollector->lateCollect();
62+
63+
// Data is serialized and unserialized by the profiler
64+
$dataCollector = unserialize(serialize($dataCollector));
65+
66+
$this->assertSame('mongodb', $dataCollector->getName());
67+
$this->assertCount(2, $dataCollector->getClients());
68+
$this->assertSame(2300, $dataCollector->getTime());
69+
$this->assertSame(3, $dataCollector->getRequestCount());
70+
$this->assertSame(1, $dataCollector->getErrorCount());
71+
72+
$requests = $dataCollector->getRequests();
73+
$this->assertSame(['client1', 'client2'], array_keys($requests));
74+
$this->assertSame([
75+
'request1' => [
76+
'clientName' => 'client1',
77+
'databaseName' => 'database1',
78+
'commandName' => 'find',
79+
'command' => ['find' => 'collection1', 'filter' => []],
80+
'durationMicros' => 1000,
81+
],
82+
'request2' => [
83+
'clientName' => 'client1',
84+
'databaseName' => 'database1',
85+
'commandName' => 'insert',
86+
'command' => ['insert' => 'collection1', 'documents' => []],
87+
'durationMicros' => 500,
88+
'error' => 'Error message',
89+
],
90+
], $requests['client1']);
91+
$this->assertSame([
92+
'request3' => [
93+
'clientName' => 'client2',
94+
'databaseName' => 'database2',
95+
'commandName' => 'aggregate',
96+
'command' => ['aggregate' => 'collection2', 'pipeline' => []],
97+
'durationMicros' => 800,
98+
],
99+
], $requests['client2']);
100+
101+
$clients = $dataCollector->getClients();
102+
$this->assertSame(['client1', 'client2'], array_keys($clients));
103+
$this->assertSame(['serverBuildInfo', 'clientInfo'], array_keys($clients['client1']));
104+
}
105+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MongoDB\Bundle\Tests\Functional\DataCollector\Stubs;
6+
7+
use MongoDB\Bundle\DataCollector\CommandEventCollector;
8+
9+
class CommandEventCollectorStub implements CommandEventCollector
10+
{
11+
public array $events;
12+
13+
public function collectCommandEvent(int $clientId, string $requestId, array $data): void
14+
{
15+
$this->events[] = ['clientId' => $clientId, 'requestId' => $requestId, 'data' => $data];
16+
}
17+
}

tests/Unit/DependencyInjection/MongoDBExtensionTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
use Symfony\Component\DependencyInjection\Definition;
3333
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
3434

35-
use function array_column;
3635
use function is_a;
3736
use function sprintf;
3837

0 commit comments

Comments
 (0)