merged branch pulzarraider/redis_profiler_storage (PR #3451)
Commits
-------
86ebe5b
Redis Profiler Storage
Discussion
----------
[HttpKernel] Redis Profiler Storage added
Bug fix: no
Feature addition: yes
Backwards compatibility break: no
Symfony2 tests pass: yes
Fixes the following tickets: -
Todo: -
Usage:
```yml
#config_dev.yml
framework:
...
profiler:
...
dsn: redis://127.0.0.1:6379
```
Redis PHP extension: https://github.com/nicolasff/phpredis
---------------------------------------------------------------------------
by fabpot at 2012-03-02T20:38:57Z
#3454 has been merged now.
---------------------------------------------------------------------------
by pulzarraider at 2012-03-02T23:41:12Z
@fabpot Tests updated and passed.
This commit is contained in:
commit
8f7218313f
@ -208,6 +208,7 @@ class FrameworkExtension extends Extension
|
||||
'mongodb' => 'Symfony\Component\HttpKernel\Profiler\MongoDbProfilerStorage',
|
||||
'memcache' => 'Symfony\Component\HttpKernel\Profiler\MemcacheProfilerStorage',
|
||||
'memcached' => 'Symfony\Component\HttpKernel\Profiler\MemcachedProfilerStorage',
|
||||
'redis' => 'Symfony\Component\HttpKernel\Profiler\RedisProfilerStorage',
|
||||
);
|
||||
list($class, ) = explode(':', $config['dsn'], 2);
|
||||
if (!isset($supported[$class])) {
|
||||
|
@ -0,0 +1,365 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpKernel\Profiler;
|
||||
|
||||
use Redis;
|
||||
|
||||
/**
|
||||
* RedisProfilerStorage stores profiling information in a Redis.
|
||||
*
|
||||
* @author Andrej Hudec <pulzarraider@gmail.com>
|
||||
*/
|
||||
class RedisProfilerStorage implements ProfilerStorageInterface
|
||||
{
|
||||
const TOKEN_PREFIX = 'sf_profiler_';
|
||||
|
||||
protected $dsn;
|
||||
protected $lifetime;
|
||||
|
||||
/**
|
||||
* @var Redis
|
||||
*/
|
||||
private $redis;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $dsn A data source name
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param int $lifetime The lifetime to use for the purge
|
||||
*/
|
||||
public function __construct($dsn, $username = '', $password = '', $lifetime = 86400)
|
||||
{
|
||||
$this->dsn = $dsn;
|
||||
$this->lifetime = (int) $lifetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function find($ip, $url, $limit, $method)
|
||||
{
|
||||
$indexName = $this->getIndexName();
|
||||
|
||||
$indexContent = $this->getValue($indexName, Redis::SERIALIZER_NONE);
|
||||
|
||||
if (!$indexContent) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$profileList = explode("\n", $indexContent);
|
||||
$result = array();
|
||||
|
||||
foreach ($profileList as $item) {
|
||||
|
||||
if ($limit === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($item == '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
list($itemToken, $itemIp, $itemMethod, $itemUrl, $itemTime, $itemParent) = explode("\t", $item, 6);
|
||||
|
||||
if ($ip && false === strpos($itemIp, $ip) || $url && false === strpos($itemUrl, $url) || $method && false === strpos($itemMethod, $method)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[$itemToken] = array(
|
||||
'token' => $itemToken,
|
||||
'ip' => $itemIp,
|
||||
'method' => $itemMethod,
|
||||
'url' => $itemUrl,
|
||||
'time' => $itemTime,
|
||||
'parent' => $itemParent,
|
||||
);
|
||||
--$limit;
|
||||
}
|
||||
|
||||
usort($result, function($a, $b) {
|
||||
if ($a['time'] === $b['time']) {
|
||||
return 0;
|
||||
}
|
||||
return $a['time'] > $b['time'] ? -1 : 1;
|
||||
});
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function purge()
|
||||
{
|
||||
//dangerous:
|
||||
//$this->getRedis()->flushDB();
|
||||
|
||||
//delete only items from index
|
||||
$indexName = $this->getIndexName();
|
||||
|
||||
$indexContent = $this->getValue($indexName, Redis::SERIALIZER_NONE);
|
||||
|
||||
if (!$indexContent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$profileList = explode("\n", $indexContent);
|
||||
|
||||
$result = array();
|
||||
|
||||
foreach ($profileList as $item) {
|
||||
|
||||
if ($item == '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pos = strpos($item, "\t");
|
||||
if (false !== $pos) {
|
||||
$result[] = $this->getItemName(substr($item, 0, $pos));
|
||||
}
|
||||
}
|
||||
|
||||
$result[] = $indexName;
|
||||
|
||||
return $this->delete($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function read($token)
|
||||
{
|
||||
if (empty($token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$profile = $this->getValue($this->getItemName($token), Redis::SERIALIZER_PHP);
|
||||
|
||||
if (false !== $profile) {
|
||||
$profile = $this->createProfileFromData($token, $profile);
|
||||
}
|
||||
|
||||
return $profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function write(Profile $profile)
|
||||
{
|
||||
$data = array(
|
||||
'token' => $profile->getToken(),
|
||||
'parent' => $profile->getParentToken(),
|
||||
'children' => array_map(function ($p) { return $p->getToken(); }, $profile->getChildren()),
|
||||
'data' => $profile->getCollectors(),
|
||||
'ip' => $profile->getIp(),
|
||||
'method' => $profile->getMethod(),
|
||||
'url' => $profile->getUrl(),
|
||||
'time' => $profile->getTime(),
|
||||
);
|
||||
|
||||
if ($this->setValue($this->getItemName($profile->getToken()), $data, $this->lifetime, Redis::SERIALIZER_PHP)) {
|
||||
// Add to index
|
||||
$indexName = $this->getIndexName();
|
||||
|
||||
$indexRow = implode("\t", array(
|
||||
$profile->getToken(),
|
||||
$profile->getIp(),
|
||||
$profile->getMethod(),
|
||||
$profile->getUrl(),
|
||||
$profile->getTime(),
|
||||
$profile->getParentToken(),
|
||||
)) . "\n";
|
||||
|
||||
return $this->appendValue($indexName, $indexRow, $this->lifetime);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal convenience method that returns the instance of Redis
|
||||
*
|
||||
* @return Redis
|
||||
*/
|
||||
protected function getRedis()
|
||||
{
|
||||
if (null === $this->redis) {
|
||||
if (!preg_match('#^redis://(?(?=\[.*\])\[(.*)\]|(.*)):(.*)$#', $this->dsn, $matches)) {
|
||||
throw new \RuntimeException('Please check your configuration. You are trying to use Redis with an invalid dsn. "' . $this->dsn . '". The expected format is redis://host:port, redis://127.0.0.1:port, redis://[::1]:port');
|
||||
}
|
||||
|
||||
$host = $matches[1]?: $matches[2];
|
||||
$port = $matches[3];
|
||||
|
||||
if (!extension_loaded('redis')) {
|
||||
throw new \RuntimeException('RedisProfilerStorage requires redis extension to be loaded.');
|
||||
}
|
||||
|
||||
$redis = new Redis;
|
||||
$redis->connect($host, $port);
|
||||
|
||||
$redis->setOption(Redis::OPT_PREFIX, self::TOKEN_PREFIX);
|
||||
|
||||
$this->redis = $redis;
|
||||
}
|
||||
|
||||
return $this->redis;
|
||||
}
|
||||
|
||||
private function createProfileFromData($token, $data, $parent = null)
|
||||
{
|
||||
$profile = new Profile($token);
|
||||
$profile->setIp($data['ip']);
|
||||
$profile->setMethod($data['method']);
|
||||
$profile->setUrl($data['url']);
|
||||
$profile->setTime($data['time']);
|
||||
$profile->setCollectors($data['data']);
|
||||
|
||||
if (!$parent && $data['parent']) {
|
||||
$parent = $this->read($data['parent']);
|
||||
}
|
||||
|
||||
if ($parent) {
|
||||
$profile->setParent($parent);
|
||||
}
|
||||
|
||||
foreach ($data['children'] as $token) {
|
||||
if (!$token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$childProfileData = $this->getValue($this->getItemName($token), Redis::SERIALIZER_PHP)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$profile->addChild($this->createProfileFromData($token, $childProfileData, $profile));
|
||||
}
|
||||
|
||||
return $profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get item name
|
||||
*
|
||||
* @param string $token
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getItemName($token)
|
||||
{
|
||||
$name = $token;
|
||||
|
||||
if ($this->isItemNameValid($name)) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name of index
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getIndexName()
|
||||
{
|
||||
$name = 'index';
|
||||
|
||||
if ($this->isItemNameValid($name)) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isItemNameValid($name)
|
||||
{
|
||||
$length = strlen($name);
|
||||
|
||||
if ($length > 2147483648) {
|
||||
throw new \RuntimeException(sprintf('The Redis item key "%s" is too long (%s bytes). Allowed maximum size is 2^31 bytes.', $name, $length));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve item from the Redis server
|
||||
*
|
||||
* @param string $key
|
||||
* @param int $serializer
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function getValue($key, $serializer = Redis::SERIALIZER_NONE)
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$redis->setOption(Redis::OPT_SERIALIZER, $serializer);
|
||||
|
||||
return $redis->get($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store an item on the Redis server under the specified key
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param int $expiration
|
||||
* @param int $serializer
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function setValue($key, $value, $expiration = 0, $serializer = Redis::SERIALIZER_NONE)
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$redis->setOption(Redis::OPT_SERIALIZER, $serializer);
|
||||
|
||||
return $redis->setex($key, $expiration, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append data to an existing item on the Redis server
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
* @param int $expiration
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function appendValue($key, $value, $expiration = 0)
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
|
||||
|
||||
if ($redis->exists($key)) {
|
||||
$redis->append($key, $value);
|
||||
return $redis->setTimeout($key, $expiration);
|
||||
}
|
||||
|
||||
return $redis->setex($key, $expiration, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove specified keys
|
||||
*
|
||||
* @param array $key
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function delete(array $keys)
|
||||
{
|
||||
return (bool) $this->getRedis()->delete($keys);
|
||||
}
|
||||
}
|
@ -183,6 +183,32 @@ abstract class AbstractProfilerStorageTest extends \PHPUnit_Framework_TestCase
|
||||
$this->getStorage()->purge();
|
||||
}
|
||||
|
||||
public function testPurge()
|
||||
{
|
||||
$profile = new Profile('token1');
|
||||
$profile->setIp('127.0.0.1');
|
||||
$profile->setUrl('http://example.com/');
|
||||
$profile->setMethod('GET');
|
||||
$this->getStorage()->write($profile);
|
||||
|
||||
$this->assertTrue(false !== $this->getStorage()->read('token1'));
|
||||
$this->assertCount(1, $this->getStorage()->find('127.0.0.1', '', 10, 'GET'));
|
||||
|
||||
$profile = new Profile('token2');
|
||||
$profile->setIp('127.0.0.1');
|
||||
$profile->setUrl('http://example.net/');
|
||||
$profile->setMethod('GET');
|
||||
$this->getStorage()->write($profile);
|
||||
|
||||
$this->assertTrue(false !== $this->getStorage()->read('token2'));
|
||||
$this->assertCount(2, $this->getStorage()->find('127.0.0.1', '', 10, 'GET'));
|
||||
|
||||
$this->getStorage()->purge();
|
||||
|
||||
$this->assertEmpty($this->getStorage()->read('token'), '->purge() removes all data stored by profiler');
|
||||
$this->assertCount(0, $this->getStorage()->find('127.0.0.1', '', 10, 'GET'), '->purge() removes all items from index');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Symfony\Component\HttpKernel\Profiler\ProfilerStorageInterface
|
||||
*/
|
||||
|
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Tests\Component\HttpKernel\Profiler;
|
||||
|
||||
use Symfony\Component\HttpKernel\Profiler\RedisProfilerStorage;
|
||||
use Symfony\Component\HttpKernel\Profiler\Profile;
|
||||
|
||||
class DummyRedisProfilerStorage extends RedisProfilerStorage
|
||||
{
|
||||
public function getRedis()
|
||||
{
|
||||
return parent::getRedis();
|
||||
}
|
||||
}
|
||||
|
||||
class RedisProfilerStorageTest extends AbstractProfilerStorageTest
|
||||
{
|
||||
protected static $storage;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
if (!extension_loaded('redis')) {
|
||||
$this->markTestSkipped('RedisProfilerStorageTest requires redis extension to be loaded');
|
||||
}
|
||||
|
||||
self::$storage = new DummyRedisProfilerStorage('redis://127.0.0.1:6379', '', '', 86400);
|
||||
try {
|
||||
self::$storage->getRedis();
|
||||
|
||||
self::$storage->purge();
|
||||
|
||||
} catch(\Exception $e) {
|
||||
self::$storage = false;
|
||||
$this->markTestSkipped('RedisProfilerStorageTest requires that there is a Redis server present on localhost');
|
||||
}
|
||||
}
|
||||
|
||||
protected function tearDown()
|
||||
{
|
||||
if (self::$storage) {
|
||||
self::$storage->purge();
|
||||
self::$storage->getRedis()->close();
|
||||
self::$storage = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Symfony\Component\HttpKernel\Profiler\ProfilerStorageInterface
|
||||
*/
|
||||
protected function getStorage()
|
||||
{
|
||||
return self::$storage;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user