-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adds Mysqli wrapper class. (#150)
* feat: adds Mysqli wrapper class. * tests: adds integration test. * chore: ignore lint for Mysqli. * chore: fix lint. * chore: fixes lint. * chore: fix lint. * chore: fixes static check. * chore: fix lint. * chore: fix lint. * chore: relax typing for being lint compliant with 7.4. * chore: disables mysqli as requirement. * chore: explicitly install mysql extension. * chore: skip test on windows. * docs: fixes suggestion in composer.json * chore: readds closer
- Loading branch information
Showing
7 changed files
with
357 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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 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 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,225 @@ | ||
<?php | ||
|
||
namespace Zipkin\Instrumentation\Mysqli; | ||
|
||
use mysqli_result; | ||
use Zipkin\Tracer; | ||
use Zipkin\Span; | ||
use Zipkin\Endpoint; | ||
|
||
use InvalidArgumentException; | ||
use const Zipkin\Tags\ERROR; | ||
|
||
/** | ||
* Mysqli is an instrumented extension for Mysqli. | ||
* Function signatures come are borrowed from | ||
* https://github.com/php/php-src/blob/master/ext/mysqli/mysqli.stub.php | ||
*/ | ||
final class Mysqli extends \Mysqli | ||
{ | ||
private const DEFAULT_OPTIONS = [ | ||
'tag_query' => false, | ||
'remote_endpoint' => null, | ||
'default_tags' => [], | ||
]; | ||
|
||
private Tracer $tracer; | ||
|
||
private array $options; | ||
|
||
public function __construct( | ||
Tracer $tracer, | ||
array $options = [], | ||
string $host = null, | ||
string $user = null, | ||
string $password = null, | ||
string $database = '', | ||
int $port = null, | ||
string $socket = null | ||
) { | ||
self::validateOptions($options); | ||
$this->tracer = $tracer; | ||
$this->options = $options + self::DEFAULT_OPTIONS; | ||
parent::__construct( | ||
$host ?? (ini_get('mysqli.default_host') ?: ''), | ||
$user ?? (ini_get('mysqli.default_user') ?: ''), | ||
$password ?? (ini_get('mysqli.default_pw') ?: ''), | ||
$database, | ||
$port ?? (($defaultPort = ini_get('mysqli.default_port')) ? (int) $defaultPort : 3306), | ||
$socket ?? (ini_get('mysqli.default_socket') ?: '') | ||
); | ||
} | ||
|
||
private static function validateOptions(array $opts): void | ||
{ | ||
if (array_key_exists('tag_query', $opts) && ($opts['tag_query'] !== (bool) $opts['tag_query'])) { | ||
throw new InvalidArgumentException('Invalid tag_query, bool expected'); | ||
} | ||
|
||
if (array_key_exists('remote_endpoint', $opts) && !($opts['remote_endpoint'] instanceof Endpoint)) { | ||
throw new InvalidArgumentException(sprintf('Invalid remote_endpoint, %s expected', Endpoint::class)); | ||
} | ||
|
||
if (array_key_exists('default_tags', $opts) && ($opts['default_tags'] !== (array) $opts['default_tags'])) { | ||
throw new InvalidArgumentException('Invalid default_tags, array expected'); | ||
} | ||
} | ||
|
||
private function addsTagsAndRemoteEndpoint(Span $span, string $query = null): void | ||
{ | ||
if ($query !== null && $this->options['tag_query']) { | ||
$span->tag('sql.query', $query); | ||
} | ||
|
||
if ($this->options['remote_endpoint'] !== null) { | ||
$span->setRemoteEndpoint($this->options['remote_endpoint']); | ||
} | ||
|
||
foreach ($this->options['default_tags'] as $key => $value) { | ||
$span->tag($key, $value); | ||
} | ||
} | ||
|
||
/** | ||
* Performs a query on the database | ||
* | ||
* @return mysqli_result|bool | ||
* @alias mysqli_query | ||
*/ | ||
public function query(string $query, int $resultmode = MYSQLI_STORE_RESULT) | ||
{ | ||
if ($resultmode === MYSQLI_ASYNC) { | ||
// if $resultmode is async, making the timing on this execution | ||
// does not make much sense. For now we just skip tracing on this. | ||
return parent::query($query, $resultmode); | ||
} | ||
|
||
$span = $this->tracer->nextSpan(); | ||
$span->setName('sql/query'); | ||
$this->addsTagsAndRemoteEndpoint($span, $query); | ||
if ($this->options['tag_query']) { | ||
$span->tag('sql.query', $query); | ||
} | ||
|
||
$span->start(); | ||
try { | ||
$result = parent::query($query, $resultmode); | ||
if ($result === false) { | ||
$span->tag(ERROR, 'true'); | ||
} | ||
return $result; | ||
} finally { | ||
$span->finish(); | ||
} | ||
} | ||
|
||
/** | ||
* @return bool | ||
* @alias mysqli_real_query | ||
*/ | ||
// phpcs:ignore PSR1.Methods.CamelCapsMethodName | ||
public function real_query(string $query) | ||
{ | ||
$span = $this->tracer->nextSpan(); | ||
$span->setName('sql/query'); | ||
$this->addsTagsAndRemoteEndpoint($span, $query); | ||
|
||
$span->start(); | ||
try { | ||
$result = parent::real_query($query); | ||
if ($result === false) { | ||
$span->tag(ERROR, 'true'); | ||
} | ||
return $result; | ||
} finally { | ||
$span->finish(); | ||
} | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
* @alias mysqli_begin_transaction | ||
*/ | ||
// phpcs:ignore PSR1.Methods.CamelCapsMethodName | ||
public function begin_transaction($flags = 0, $name = null) | ||
{ | ||
$span = $this->tracer->nextSpan(); | ||
$span->setName('sql/begin_transaction'); | ||
$this->addsTagsAndRemoteEndpoint($span); | ||
$span->start(); | ||
if ($name !== null) { | ||
$span->tag('mysqli.transaction_name', (string) $name); | ||
} | ||
try { | ||
if ($name === null) { | ||
$result = parent::begin_transaction($flags); | ||
} else { | ||
$result = parent::begin_transaction($flags, $name); | ||
} | ||
|
||
if ($result === false) { | ||
$span->tag(ERROR, 'true'); | ||
} | ||
return $result; | ||
} finally { | ||
$span->finish(); | ||
} | ||
} | ||
|
||
/** | ||
* @return bool | ||
* @alias mysqli_commit | ||
*/ | ||
public function commit(int $flags = -1, ?string $name = null) | ||
{ | ||
$span = $this->tracer->nextSpan(); | ||
$span->setName('sql/begin_transaction'); | ||
$this->addsTagsAndRemoteEndpoint($span); | ||
$span->start(); | ||
if ($name !== null) { | ||
$span->tag('mysqli.transaction_name', $name); | ||
} | ||
try { | ||
if ($name === null) { | ||
$result = parent::commit($flags); | ||
} else { | ||
$result = parent::commit($flags, $name); | ||
} | ||
|
||
if ($result === false) { | ||
$span->tag(ERROR, 'true'); | ||
} | ||
return $result; | ||
} finally { | ||
$span->finish(); | ||
} | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
* @alias mysqli_rollback | ||
*/ | ||
public function rollback($flags = 0, $name = null) | ||
{ | ||
$span = $this->tracer->nextSpan(); | ||
$span->setName('sql/rollback'); | ||
$this->addsTagsAndRemoteEndpoint($span); | ||
$span->start(); | ||
if ($name !== null) { | ||
$span->tag('mysqli.transaction_name', (string) $name); | ||
} | ||
try { | ||
if ($name === null) { | ||
$result = parent::commit($flags); | ||
} else { | ||
$result = parent::commit($flags, $name); | ||
} | ||
if ($result === false) { | ||
$span->tag(ERROR, 'true'); | ||
} | ||
return $result; | ||
} finally { | ||
$span->finish(); | ||
} | ||
} | ||
} |
This file contains 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,19 @@ | ||
# Zipkin instrumentation for Mysqli | ||
|
||
```php | ||
use Zipkin\Instrumentation\Mysqli\Mysqli; | ||
|
||
$mysqli = new Mysqli($tracer, [], "127.0.0.1", "my_user", "my_password", "sakila"); | ||
|
||
if ($mysqli->connect_errno) { | ||
printf("Connect failed: %s\n", $mysqli->connect_error); | ||
exit(); | ||
} | ||
|
||
$mysqli->begin_transaction(MYSQLI_TRANS_START_READ_ONLY); | ||
|
||
$mysqli->query("SELECT first_name, last_name FROM actor"); | ||
$mysqli->commit(); | ||
|
||
$mysqli->close(); | ||
``` |
This file contains 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,88 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace ZipkinTests\Integration\Instrumentation\Http\Server; | ||
|
||
use Zipkin\Tracer; | ||
use Zipkin\Samplers\BinarySampler; | ||
use Zipkin\Reporters\InMemory; | ||
use Zipkin\Propagation\CurrentTraceContext; | ||
use Zipkin\Instrumentation\Mysqli\Mysqli; | ||
use Zipkin\Endpoint; | ||
use Prophecy\PhpUnit\ProphecyTrait; | ||
use PHPUnit\Framework\TestCase; | ||
|
||
final class MysqliTest extends TestCase | ||
{ | ||
use ProphecyTrait; | ||
|
||
private static function launchMySQL(): array | ||
{ | ||
shell_exec('docker rm -f zipkin_php_mysql_test'); | ||
shell_exec(sprintf('cd %s; docker-compose up -d', __DIR__)); | ||
echo "Waiting for mysql container to be up."; | ||
while (true) { | ||
$res = shell_exec('docker ps --filter "name=zipkin_php_mysql_test" --format "{{.Status}}"'); | ||
if (strpos($res, "healthy") >= 0) { | ||
break; | ||
} | ||
} | ||
|
||
$host = '127.0.0.1'; | ||
$user = 'root'; | ||
$pass = 'root'; | ||
$db = 'test'; | ||
$port = 3306; | ||
|
||
return [[$host, $user, $pass, $db, $port], function () { | ||
shell_exec(sprintf('cd %s; docker-compose stop', __DIR__)); | ||
}]; | ||
} | ||
|
||
public function testConnect() | ||
{ | ||
if (PHP_OS_FAMILY === 'Windows') { | ||
$this->markTestSkipped("Running the test on windows might be problematic"); | ||
} | ||
|
||
if (!extension_loaded("mysqli")) { | ||
$this->markTestSkipped("mysqli isn't loaded"); | ||
} | ||
|
||
list($params, $closer) = self::launchMySQL(); | ||
|
||
$reporter = new InMemory(); | ||
|
||
$tracer = new Tracer( | ||
Endpoint::createAsEmpty(), | ||
$reporter, | ||
BinarySampler::createAsAlwaysSample(), | ||
false, // usesTraceId128bits | ||
new CurrentTraceContext(), | ||
false // isNoop | ||
); | ||
|
||
try { | ||
$mysqli = new Mysqli($tracer, [], ...$params); | ||
|
||
if ($mysqli->connect_errno) { | ||
$this->fail( | ||
sprintf('Failed to connect to MySQL: %s %s', $mysqli->connect_errno, $mysqli->connect_error) | ||
); | ||
} | ||
|
||
$res = $mysqli->query('SELECT 1'); | ||
$this->assertEquals(1, $res->num_rows); | ||
|
||
$tracer->flush(); | ||
$spans = $reporter->flush(); | ||
$this->assertEquals(1, count($spans)); | ||
|
||
$span = $spans[0]; | ||
$this->assertEquals('sql/query', $span->getName()); | ||
} finally { | ||
$closer(); | ||
} | ||
} | ||
} |
This file contains 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,4 @@ | ||
|
||
[mysqld] | ||
# makes it possible to connect to the server using `mysql` as host | ||
bind-address = 0.0.0.0 |
18 changes: 18 additions & 0 deletions
18
tests/Integration/Instrumentation/Mysqli/docker-compose.yaml
This file contains 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,18 @@ | ||
version: "2.4" | ||
|
||
services: | ||
mysql: | ||
image: mysql:latest | ||
container_name: zipkin_php_mysql_test | ||
environment: | ||
- MYSQL_ROOT_PASSWORD=root | ||
- MYSQL_DATABASE=test | ||
- MYSQL_ALLOW_EMPTY_PASSWORD=yes | ||
volumes: | ||
- ./access.cnf:/etc/mysql/conf.d/access.cnf | ||
ports: | ||
- "3306:3306" | ||
healthcheck: | ||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] | ||
timeout: 20s | ||
retries: 10 |