Skip to content

Commit 2c77cf1

Browse files
author
anahan
committed
Initial prototype
1 parent 098c678 commit 2c77cf1

File tree

7 files changed

+397
-0
lines changed

7 files changed

+397
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
.idea
12
composer.phar
3+
composer.lock
24
/vendor/
35

46
# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file

composer.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"require": {
3+
"php": "^7.2",
4+
"ext-mongodb": "*",
5+
"code-tool/jaeger-client-php": "^2.1.4"
6+
},
7+
"autoload": {
8+
"psr-4": {
9+
"CodeTool\\Jaeger\\MongoDb\\": "src/"
10+
}
11+
},
12+
"autoload-dev": {
13+
"psr-4": {
14+
"CodeTool\\Jaeger\\Tests\\MongoDb\\": "test/"
15+
}
16+
},
17+
"require-dev": {
18+
"phpunit/phpunit": "^6.5.5"
19+
}
20+
}

phpunit.xml.dist

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
4+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.5/phpunit.xsd"
6+
backupGlobals="false"
7+
colors="true"
8+
>
9+
<php>
10+
<ini name="error_reporting" value="-1" />
11+
<server name="KERNEL_DIR" value="app/" />
12+
</php>
13+
14+
<testsuites>
15+
<testsuite name="Project Test Suite">
16+
<directory>test</directory>
17+
</testsuite>
18+
</testsuites>
19+
20+
<filter>
21+
<whitelist>
22+
<directory>src</directory>
23+
<exclude>
24+
<directory>src/*Bundle/Resources</directory>
25+
<directory>src/*/*Bundle/Resources</directory>
26+
<directory>src/*/Bundle/*Bundle/Resources</directory>
27+
</exclude>
28+
</whitelist>
29+
</filter>
30+
</phpunit>

src/CommandToStringConvertor.php

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace CodeTool\Jaeger\MongoDb;
6+
7+
use MongoDB\BSON\Type;
8+
9+
class CommandToStringConvertor
10+
{
11+
/**
12+
* @var int
13+
*/
14+
private $bsonEndPos;
15+
16+
public function __construct()
17+
{
18+
$this->bsonEndPos = strrpos(Type::class, '\\') + 1;
19+
}
20+
21+
private function transformScalar($v): string
22+
{
23+
if (\is_int($v)) {
24+
return '?';
25+
}
26+
27+
if (\is_array($v)) {
28+
return $this->transformQuery($v);
29+
}
30+
31+
if (\is_object($v)) {
32+
if ($v instanceof Type) {
33+
return 'new ' . substr(\get_class($v), $this->bsonEndPos) . '(?)';
34+
}
35+
36+
return $this->transformQuery($v);
37+
}
38+
39+
if (\is_string($v) && $v !== '' && $v[0] === '$') {
40+
return '"' . $v . '"';
41+
}
42+
43+
return '"?"';
44+
}
45+
46+
private function transformQuery($data): string
47+
{
48+
$isArray = \is_array($data);
49+
$result = $isArray ? '[' : '{';
50+
51+
foreach ($data as $k => $v) {
52+
if (false === $isArray) {
53+
$result .= $k . ': ';
54+
55+
if ($k === '$in') {
56+
$result .= '[\'...\'], ';
57+
58+
continue;
59+
}
60+
}
61+
62+
$result .= $this->transformScalar($v) . ', ';
63+
}
64+
65+
return substr($result, 0, -2) . ($isArray ? ']' : '}');
66+
}
67+
68+
private function simpleScalarToJson($v): string
69+
{
70+
if ($v === true) {
71+
return 'true';
72+
}
73+
74+
if ($v === false) {
75+
return 'true';
76+
}
77+
78+
if ($v === null) {
79+
return 'null';
80+
}
81+
82+
if (\is_int($v)) {
83+
return (string)$v;
84+
}
85+
86+
return '"' . $v . '"';
87+
}
88+
89+
private function serializeWriteConcern($writeConcern): string
90+
{
91+
if (!isset($writeConcern->w)) {
92+
return '';
93+
}
94+
95+
$result = '{w: ' . $this->simpleScalarToJson($writeConcern->w);
96+
if (isset($writeConcern->j)) {
97+
$result .= ', j: ' . $this->simpleScalarToJson($writeConcern->wtimeout);
98+
}
99+
100+
if (isset($writeConcern->wtimeout)) {
101+
$result .= ', wtimeout: ' . $this->simpleScalarToJson($writeConcern->wtimeout);
102+
}
103+
104+
$result .= '}';
105+
106+
return $result;
107+
}
108+
109+
private function transformUpdateDelete(object $op): string
110+
{
111+
$result = '{';
112+
foreach ($op as $k => $v) {
113+
switch ($k) {
114+
case 'q':
115+
case 'u':
116+
$tVal = $this->transformQuery($v);
117+
break;
118+
default:
119+
$tVal = json_encode($v);
120+
}
121+
122+
$result .= $k . ': ' . $tVal . ', ';
123+
}
124+
125+
return substr($result, 0, -2) . '}';
126+
}
127+
128+
private function transformUpdatesDeletes(array $coll): string
129+
{
130+
$result = '[';
131+
foreach ($coll as $v) {
132+
$result .= $this->transformUpdateDelete($v) . ', ';
133+
}
134+
135+
return substr($result, 0, -2) . ']';
136+
}
137+
138+
public function convert(object $command): string
139+
{
140+
$result = 'db.runCommand({';
141+
foreach ($command as $k => $v) {
142+
switch ($k) {
143+
case 'updates':
144+
case 'deletes':
145+
$tVal = $this->transformUpdatesDeletes($v);
146+
break;
147+
148+
case 'query':
149+
case 'filter':
150+
case 'documents':
151+
$tVal = $this->transformQuery($v);
152+
break;
153+
154+
case 'writeConcern':
155+
$tVal = $this->serializeWriteConcern($v);
156+
break;
157+
default:
158+
$tVal = json_encode($v);
159+
}
160+
161+
if ('' === $tVal) {
162+
continue;
163+
}
164+
$result .= $k . ': ' . $tVal . ', ';
165+
}
166+
167+
return substr($result, 0, -2) . '})';
168+
}
169+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CodeTool\Jaeger\MongoDb;
6+
7+
use Jaeger\Log\ErrorLog;
8+
use Jaeger\Span\SpanInterface;
9+
use Jaeger\Tag\BoolTag;
10+
use Jaeger\Tag\ComponentTag;
11+
use Jaeger\Tag\DbInstanceTag;
12+
use Jaeger\Tag\DbStatementTag;
13+
use Jaeger\Tag\DbType;
14+
use Jaeger\Tag\PeerHostnameTag;
15+
use Jaeger\Tag\PeerPortTag;
16+
use Jaeger\Tag\SpanKindClientTag;
17+
use Jaeger\Tracer\TracerInterface;
18+
use MongoDB\Driver\Monitoring\CommandFailedEvent;
19+
use MongoDB\Driver\Monitoring\CommandStartedEvent;
20+
use MongoDB\Driver\Monitoring\CommandSubscriber;
21+
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
22+
23+
class JaegerMongoDbQueryTimeCollector implements CommandSubscriber
24+
{
25+
private $tracer;
26+
27+
/**
28+
* @var SpanInterface[]
29+
*/
30+
private $requestIdToSpan = [];
31+
32+
/**
33+
* @var CommandToStringConvertor
34+
*/
35+
private $convertor;
36+
37+
public function __construct(TracerInterface $tracer, CommandToStringConvertor $convertor)
38+
{
39+
$this->tracer = $tracer;
40+
$this->convertor = $convertor;
41+
}
42+
43+
public function commandStarted(CommandStartedEvent $event)
44+
{
45+
/** @var MongoDB\Driver\Server $server */
46+
$server = $event->getServer();
47+
48+
$this->requestIdToSpan[$event->getRequestId()] = $this->tracer->start(
49+
sprintf('mongodb.%s', $event->getCommandName()),
50+
[
51+
new SpanKindClientTag(),
52+
new ComponentTag('php-mongodb'),
53+
54+
new DbType('mongo'),
55+
new DbInstanceTag($event->getDatabaseName()),
56+
new DbStatementTag($this->convertor->convert($event->getCommand())),
57+
58+
new PeerHostnameTag($server->getHost()),
59+
new PeerPortTag($server->getPort()),
60+
]
61+
);
62+
}
63+
64+
private function getSpanByEvent($event): ?SpanInterface
65+
{
66+
if (false === array_key_exists($event->getRequestId(), $this->requestIdToSpan)) {
67+
// warning, should not happen!
68+
return null;
69+
}
70+
71+
$span = $this->requestIdToSpan[$event->getRequestId()];
72+
unset($this->requestIdToSpan[$event->getRequestId()]);
73+
74+
return $span;
75+
}
76+
77+
public function commandFailed(CommandFailedEvent $event)
78+
{
79+
if (null === $span = $this->getSpanByEvent($event)) {
80+
return;
81+
}
82+
83+
$span->addTag(new BoolTag('error', true));
84+
$span->addLog(new ErrorLog($event->getError()->getMessage(), $event->getError()->getTraceAsString()));
85+
86+
$this->tracer->finish($span, $event->getDurationMicros());
87+
}
88+
89+
public function commandSucceeded(CommandSucceededEvent $event)
90+
{
91+
if (null === $span = $this->getSpanByEvent($event)) {
92+
return;
93+
}
94+
95+
$this->tracer->finish($span, $event->getDurationMicros());
96+
}
97+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace CodeTool\Jaeger\Tests\MongoDb;
6+
7+
use CodeTool\Jaeger\MongoDb\CommandToStringConvertor;
8+
use MongoDB\BSON\UTCDateTime;
9+
use PHPUnit\Framework\TestCase;
10+
11+
final class CommandToStringConvertorTest extends TestCase
12+
{
13+
public function dataProvider()
14+
{
15+
return [
16+
[
17+
['query' => ['player_id' => 34]],
18+
'db.runCommand({query: {player_id: ?}})'
19+
],
20+
[
21+
['query' => ['player_id' => '34']],
22+
'db.runCommand({query: {player_id: "?"}})'
23+
],
24+
[
25+
['query' => ['player_id' => ['$in' => ['34', '343']]]],
26+
'db.runCommand({query: {player_id: {$in: [\'...\']}}})'
27+
],
28+
[
29+
new OVal(
30+
'query',
31+
new OVal(
32+
'c',
33+
new OVal('$gte', new UTCDateTime(1000000), '$lte', '$now')
34+
)
35+
),
36+
'db.runCommand({query: {c: {$gte: new UTCDateTime(?), $lte: "$now"}}})'
37+
],
38+
];
39+
}
40+
41+
private function array2object($array)
42+
{
43+
return json_decode(json_encode($array));
44+
}
45+
46+
/**
47+
* @dataProvider dataProvider
48+
*
49+
* @param array|\stdClass $command
50+
* @param string $expected
51+
*/
52+
public function testConvert($command, string $expected): void
53+
{
54+
$convertor = new CommandToStringConvertor();
55+
56+
$data = $command;
57+
if (\is_array($command)) {
58+
$data = $this->array2object($command);
59+
}
60+
61+
$this->assertEquals($expected, $convertor->convert($data));
62+
}
63+
}

0 commit comments

Comments
 (0)