Skip to content

Commit

Permalink
Redis Profiler Storage
Browse files Browse the repository at this point in the history
fixed typo and tests

- updated profiler tests
- added testPurge() method
- fixed find() method
  • Loading branch information
pulzarraider committed Mar 2, 2012
1 parent ddeac9a commit 86ebe5b
Show file tree
Hide file tree
Showing 4 changed files with 455 additions and 0 deletions.
Expand Up @@ -208,6 +208,7 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $
'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])) {
Expand Down
365 changes: 365 additions & 0 deletions src/Symfony/Component/HttpKernel/Profiler/RedisProfilerStorage.php
@@ -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);
}
}

0 comments on commit 86ebe5b

Please sign in to comment.