Skip to content

Commit

Permalink
Merge pull request #578 from DataDog/labbati/sandboxing-memcached
Browse files Browse the repository at this point in the history
Migrate memcached integration to sandboxed API
  • Loading branch information
SammyK committed Sep 18, 2019
2 parents 8c3ba51 + 0626bed commit c35e045
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 8 deletions.
1 change: 1 addition & 0 deletions bridge/dd_require_all.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
require __DIR__ . '/../src/DDTrace/Integrations/Eloquent/EloquentIntegration.php';
require __DIR__ . '/../src/DDTrace/Integrations/Eloquent/EloquentSandboxedIntegration.php';
require __DIR__ . '/../src/DDTrace/Integrations/Memcached/MemcachedIntegration.php';
require __DIR__ . '/../src/DDTrace/Integrations/Memcached/MemcachedSandboxedIntegration.php';
require __DIR__ . '/../src/DDTrace/Integrations/Curl/CurlIntegration.php';
require __DIR__ . '/../src/DDTrace/Integrations/Mysqli/MysqliIntegration.php';
require __DIR__ . '/../src/DDTrace/Integrations/Mongo/MongoClientIntegration.php';
Expand Down
3 changes: 3 additions & 0 deletions src/DDTrace/Integrations/IntegrationsLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use DDTrace\Integrations\Laravel\LaravelIntegration;
use DDTrace\Integrations\Lumen\LumenIntegration;
use DDTrace\Integrations\Memcached\MemcachedIntegration;
use DDTrace\Integrations\Memcached\MemcachedSandboxedIntegration;
use DDTrace\Integrations\Mongo\MongoIntegration;
use DDTrace\Integrations\Mysqli\MysqliIntegration;
use DDTrace\Integrations\PDO\PDOIntegration;
Expand Down Expand Up @@ -77,6 +78,8 @@ public function __construct(array $integrations)
if (Configuration::get()->isSandboxEnabled()) {
$this->integrations[EloquentSandboxedIntegration::NAME] =
'\DDTrace\Integrations\Eloquent\EloquentSandboxedIntegration';
$this->integrations[MemcachedSandboxedIntegration::NAME] =
'\DDTrace\Integrations\Memcached\MemcachedSandboxedIntegration';
$this->integrations[PDOSandboxedIntegration::NAME] =
'\DDTrace\Integrations\PDO\PDOSandboxedIntegration';
}
Expand Down
254 changes: 254 additions & 0 deletions src/DDTrace/Integrations/Memcached/MemcachedSandboxedIntegration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
<?php

namespace DDTrace\Integrations\Memcached;

use DDTrace\Integrations\Integration;
use DDTrace\Integrations\SandboxedIntegration;
use DDTrace\Obfuscation;
use DDTrace\SpanData;
use DDTrace\Tag;
use DDTrace\Type;

/**
* Tracing of the Memcached library.
*
* Not currently dealt with: getDelayed(Multi) and fetch(All). Could be added;
* would probably want to wrap the callback to getDelayed(Multi) if it is
* present as well.
*
* Also not wrapped: callables passed to get()/getByKey()
*
* setMulti and deleteMulti don't generate out.host and out.port because it
* might be different for each key. setMultiByKey does, since you're pinning a
* specific server.
*/
class MemcachedSandboxedIntegration extends SandboxedIntegration
{
const NAME = 'memcached';

/**
* @var self
*/
private static $instance;

/**
* @return self
*/
public static function getInstance()
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}

/**
* @return string The integration name.
*/
public function getName()
{
return self::NAME;
}

public function init()
{
if (!extension_loaded('memcached')) {
// Memcached is provided through an extension and not through a class loader.
return Integration::NOT_AVAILABLE;
}
$integration = $this;

$this->traceCommand('add');
$this->traceCommandByKey('addByKey');

$this->traceCommand('append');
$this->traceCommandByKey('appendByKey');

$this->traceCommand('decrement');
$this->traceCommandByKey('decrementByKey');

$this->traceCommand('delete');
$this->traceMulti('deleteMulti');
$this->traceCommandByKey('deleteByKey');
$this->traceMultiByKey('deleteMultiByKey');

$this->traceCommand('get');
$this->traceMulti('getMulti');
$this->traceCommandByKey('getByKey');
$this->traceMultiByKey('getMultiByKey');

$this->traceCommand('set');
$this->traceMulti('setMulti');
$this->traceCommandByKey('setByKey');
$this->traceMultiByKey('setMultiByKey');

$this->traceCommand('increment');
$this->traceCommandByKey('incrementByKey');

$this->traceCommand('prepend');
$this->traceCommandByKey('prependByKey');

$this->traceCommand('replace');
$this->traceCommandByKey('replaceByKey');

$this->traceCommand('touch');
$this->traceCommandByKey('touchByKey');

dd_trace_method('Memcached', 'flush', function (SpanData $span) use ($integration) {
if (dd_trace_tracer_is_limited()) {
return false;
}
$integration->setCommonData($span, 'flush');
});

dd_trace_method('Memcached', 'cas', function (SpanData $span, $args) use ($integration) {
if (dd_trace_tracer_is_limited()) {
return false;
}
$integration->setCommonData($span, 'cas');
$span->meta['memcached.cas_token'] = $args[0];
$span->meta['memcached.query'] = 'cas ?';
$integration->setServerTagsByKey($span, $this, $args[1]);
});

dd_trace_method('Memcached', 'casByKey', function (SpanData $span, $args) use ($integration) {
if (dd_trace_tracer_is_limited()) {
return false;
}
$integration->setCommonData($span, 'casByKey');
$span->meta['memcached.cas_token'] = $args[0];
$span->meta['memcached.query'] = 'casByKey ?';
$span->meta['memcached.server_key'] = $args[1];

$integration->setServerTagsByKey($span, $this, $args[0]);
});

return Integration::LOADED;
}

public function traceCommand($command)
{
$integration = $this;
dd_trace_method('Memcached', $command, function (SpanData $span, $args) use ($integration, $command) {
if (dd_trace_tracer_is_limited()) {
return false;
}
$integration->setCommonData($span, $command);
if (!is_array($args[0])) {
$integration->setServerTagsByKey($span, $this, $args[0]);
$span->meta['memcached.query'] = $command . ' ' . Obfuscation::toObfuscatedString($args[0]);
}

$integration->markForTraceAnalytics($span, $command);
});
}

public function traceCommandByKey($command)
{
$integration = $this;
dd_trace_method('Memcached', $command, function (SpanData $span, $args) use ($integration, $command) {
if (dd_trace_tracer_is_limited()) {
return false;
}
$integration->setCommonData($span, $command);
if (!is_array($args[0])) {
$integration->setServerTagsByKey($span, $this, $args[0]);
$span->meta['memcached.query'] = $command . ' ' . Obfuscation::toObfuscatedString($args[0]);
$span->meta['memcached.server_key'] = (string)$args[0];
}

$integration->markForTraceAnalytics($span, $command);
});
}

public function traceMulti($command)
{
$integration = $this;
dd_trace_method('Memcached', $command, function (SpanData $span, $args) use ($integration, $command) {
if (dd_trace_tracer_is_limited()) {
return false;
}
$integration->setCommonData($span, $command);
if (!is_array($args[0])) {
$integration->setServerTagsByKey($span, $this, $args[0]);
}
$span->meta['memcached.query'] = $command . ' ' . Obfuscation::toObfuscatedString($args[0], ',');
$integration->markForTraceAnalytics($span, $command);
});
}

public function traceMultiByKey($command)
{
$integration = $this;
dd_trace_method('Memcached', $command, function (SpanData $span, $args) use ($integration, $command) {
if (dd_trace_tracer_is_limited()) {
return false;
}
$integration->setCommonData($span, $command);
$span->meta['memcached.server_key'] = (string)$args[0];
$integration->setServerTagsByKey($span, $this, $args[0]);
$query = "$command " . Obfuscation::toObfuscatedString($args[1], ',');
$span->meta['memcached.query'] = $query;
$integration->markForTraceAnalytics($span, $command);
});
}

/**
* Sets common values shared by many commands.
*
* @param SpanData $span
* @param string $command
*/
public function setCommonData(SpanData $span, $command)
{
$span->name = "Memcached.$command";
$span->type = Type::MEMCACHED;
$span->service = 'memcached';
$span->resource = $command;
$span->meta['memcached.command'] = $command;
}

/**
* Memcached::getServerByKey() /might/ return incorrect information if the
* distribution would be rebuilt on a real call (Memcached::get(),
* Memcached::getByKey(), and other commands that actually hit the server
* include logic to regenerate the distribution if a server has been ejected
* or if a timer expires; Memcached::getServerByKey() does not check for the
* distribution being rebuilt. Getting around that would likely be
* prohibitively expensive though.
*/
public function setServerTagsByKey(SpanData $span, $memcached, $key)
{
$server = $memcached->getServerByKey($key);

// getServerByKey() might return `false`: https://www.php.net/manual/en/memcached.getserverbykey.php
if (!is_array($server)) {
return;
}

$span->meta[Tag::TARGET_HOST] = $server['host'];
$span->meta[Tag::TARGET_PORT] = (string)$server['port'];
}

/**
* @param SpanData $span
* @param string $command
*/
public function markForTraceAnalytics(SpanData $span, $command)
{
$commandsForAnalytics = [
'add',
'addByKey',
'delete',
'deleteByKey',
'get',
'getByKey',
'set',
'setByKey',
];

if (in_array($command, $commandsForAnalytics)) {
$this->addTraceAnalyticsIfEnabled($span);
}
}
}
2 changes: 2 additions & 0 deletions tests/Common/TracerTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ trait TracerTestTrait
*/
public function isolateTracer($fn, $tracer = null)
{
// Reset the current C-level array of generated spans
dd_trace_serialize_closed_spans();
$transport = new DebugTransport();
$tracer = $tracer ?: new Tracer($transport);
GlobalTracer::set($tracer);
Expand Down
8 changes: 8 additions & 0 deletions tests/Integrations/Memcached/MemcachedSandboxedTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace DDTrace\Tests\Integrations\Memcached;

final class MemcachedSandboxedTest extends MemcachedTest
{
const IS_SANDBOX = true;
}
61 changes: 53 additions & 8 deletions tests/Integrations/Memcached/MemcachedTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

namespace DDTrace\Tests\Integrations\Memcached;

use DDTrace\Integrations\IntegrationsLoader;
use DDTrace\Obfuscation;
use DDTrace\Tests\Common\IntegrationTestCase;
use DDTrace\Tests\Common\SpanAssertion;
use DDTrace\Util\Versions;


final class MemcachedTest extends IntegrationTestCase
class MemcachedTest extends IntegrationTestCase
{
const IS_SANDBOX = false;

/**
* @var \Memcached
*/
Expand All @@ -18,12 +20,6 @@ final class MemcachedTest extends IntegrationTestCase
private static $host = 'memcached_integration';
private static $port = '11211';

public static function setUpBeforeClass()
{
parent::setUpBeforeClass();
IntegrationsLoader::load();
}

protected function setUp()
{
parent::setUp();
Expand Down Expand Up @@ -662,6 +658,55 @@ public function testTouchByKey()
]);
}

public function testCas()
{
$this->client->set('ip_block', 'some_value');
if (Versions::phpVersionMatches('5.4') || Versions::phpVersionMatches('5.6')) {
$cas = null;
$this->client->get('ip_block', null, $cas);
} else {
$result = $this->client->get('ip_block', null, \Memcached::GET_EXTENDED);
$cas = $result['cas'];
}
$traces = $this->isolateTracer(function () use ($cas) {
$this->client->cas($cas, 'key', 'value');
});
$this->assertSpans($traces, [
SpanAssertion::build('Memcached.cas', 'memcached', 'memcached', 'cas')
->setTraceAnalyticsCandidate()
->withExactTags(array_merge(self::baseTags(), [
'memcached.query' => 'cas ' . Obfuscation::toObfuscatedString('key'),
'memcached.command' => 'cas',
]))
->withExistingTagsNames(['memcached.cas_token']),
]);
}

public function testCasByKey()
{
$this->client->setByKey('my_server', 'ip_block', 'some_value');
if (Versions::phpVersionMatches('5.4') || Versions::phpVersionMatches('5.6')) {
$cas = null;
$this->client->getByKey('my_server', 'ip_block', null, $cas);
} else {
$result = $this->client->getByKey('my_server', 'ip_block', null, \Memcached::GET_EXTENDED);
$cas = $result['cas'];
}
$traces = $this->isolateTracer(function () use ($cas) {
$this->client->casByKey($cas, 'my_server', 'key', 'value');
});
$this->assertSpans($traces, [
SpanAssertion::build('Memcached.casByKey', 'memcached', 'memcached', 'casByKey')
->setTraceAnalyticsCandidate()
->withExactTags(array_merge(self::baseTags(), [
'memcached.query' => 'casByKey ' . Obfuscation::toObfuscatedString('key'),
'memcached.command' => 'casByKey',
'memcached.server_key' => 'my_server',
]))
->withExistingTagsNames(['memcached.cas_token']),
]);
}

private static function baseTags()
{
return [
Expand Down

0 comments on commit c35e045

Please sign in to comment.