Skip to content

Commit 86ebe5b

Browse files
committed
Redis Profiler Storage
fixed typo and tests - updated profiler tests - added testPurge() method - fixed find() method
1 parent ddeac9a commit 86ebe5b

File tree

4 files changed

+455
-0
lines changed

4 files changed

+455
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $
208208
'mongodb' => 'Symfony\Component\HttpKernel\Profiler\MongoDbProfilerStorage',
209209
'memcache' => 'Symfony\Component\HttpKernel\Profiler\MemcacheProfilerStorage',
210210
'memcached' => 'Symfony\Component\HttpKernel\Profiler\MemcachedProfilerStorage',
211+
'redis' => 'Symfony\Component\HttpKernel\Profiler\RedisProfilerStorage',
211212
);
212213
list($class, ) = explode(':', $config['dsn'], 2);
213214
if (!isset($supported[$class])) {
Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Profiler;
13+
14+
use Redis;
15+
16+
/**
17+
* RedisProfilerStorage stores profiling information in a Redis.
18+
*
19+
* @author Andrej Hudec <pulzarraider@gmail.com>
20+
*/
21+
class RedisProfilerStorage implements ProfilerStorageInterface
22+
{
23+
const TOKEN_PREFIX = 'sf_profiler_';
24+
25+
protected $dsn;
26+
protected $lifetime;
27+
28+
/**
29+
* @var Redis
30+
*/
31+
private $redis;
32+
33+
/**
34+
* Constructor.
35+
*
36+
* @param string $dsn A data source name
37+
* @param string $username
38+
* @param string $password
39+
* @param int $lifetime The lifetime to use for the purge
40+
*/
41+
public function __construct($dsn, $username = '', $password = '', $lifetime = 86400)
42+
{
43+
$this->dsn = $dsn;
44+
$this->lifetime = (int) $lifetime;
45+
}
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
public function find($ip, $url, $limit, $method)
51+
{
52+
$indexName = $this->getIndexName();
53+
54+
$indexContent = $this->getValue($indexName, Redis::SERIALIZER_NONE);
55+
56+
if (!$indexContent) {
57+
return array();
58+
}
59+
60+
$profileList = explode("\n", $indexContent);
61+
$result = array();
62+
63+
foreach ($profileList as $item) {
64+
65+
if ($limit === 0) {
66+
break;
67+
}
68+
69+
if ($item == '') {
70+
continue;
71+
}
72+
73+
list($itemToken, $itemIp, $itemMethod, $itemUrl, $itemTime, $itemParent) = explode("\t", $item, 6);
74+
75+
if ($ip && false === strpos($itemIp, $ip) || $url && false === strpos($itemUrl, $url) || $method && false === strpos($itemMethod, $method)) {
76+
continue;
77+
}
78+
79+
$result[$itemToken] = array(
80+
'token' => $itemToken,
81+
'ip' => $itemIp,
82+
'method' => $itemMethod,
83+
'url' => $itemUrl,
84+
'time' => $itemTime,
85+
'parent' => $itemParent,
86+
);
87+
--$limit;
88+
}
89+
90+
usort($result, function($a, $b) {
91+
if ($a['time'] === $b['time']) {
92+
return 0;
93+
}
94+
return $a['time'] > $b['time'] ? -1 : 1;
95+
});
96+
97+
return $result;
98+
}
99+
100+
/**
101+
* {@inheritdoc}
102+
*/
103+
public function purge()
104+
{
105+
//dangerous:
106+
//$this->getRedis()->flushDB();
107+
108+
//delete only items from index
109+
$indexName = $this->getIndexName();
110+
111+
$indexContent = $this->getValue($indexName, Redis::SERIALIZER_NONE);
112+
113+
if (!$indexContent) {
114+
return false;
115+
}
116+
117+
$profileList = explode("\n", $indexContent);
118+
119+
$result = array();
120+
121+
foreach ($profileList as $item) {
122+
123+
if ($item == '') {
124+
continue;
125+
}
126+
127+
$pos = strpos($item, "\t");
128+
if (false !== $pos) {
129+
$result[] = $this->getItemName(substr($item, 0, $pos));
130+
}
131+
}
132+
133+
$result[] = $indexName;
134+
135+
return $this->delete($result);
136+
}
137+
138+
/**
139+
* {@inheritdoc}
140+
*/
141+
public function read($token)
142+
{
143+
if (empty($token)) {
144+
return false;
145+
}
146+
147+
$profile = $this->getValue($this->getItemName($token), Redis::SERIALIZER_PHP);
148+
149+
if (false !== $profile) {
150+
$profile = $this->createProfileFromData($token, $profile);
151+
}
152+
153+
return $profile;
154+
}
155+
156+
/**
157+
* {@inheritdoc}
158+
*/
159+
public function write(Profile $profile)
160+
{
161+
$data = array(
162+
'token' => $profile->getToken(),
163+
'parent' => $profile->getParentToken(),
164+
'children' => array_map(function ($p) { return $p->getToken(); }, $profile->getChildren()),
165+
'data' => $profile->getCollectors(),
166+
'ip' => $profile->getIp(),
167+
'method' => $profile->getMethod(),
168+
'url' => $profile->getUrl(),
169+
'time' => $profile->getTime(),
170+
);
171+
172+
if ($this->setValue($this->getItemName($profile->getToken()), $data, $this->lifetime, Redis::SERIALIZER_PHP)) {
173+
// Add to index
174+
$indexName = $this->getIndexName();
175+
176+
$indexRow = implode("\t", array(
177+
$profile->getToken(),
178+
$profile->getIp(),
179+
$profile->getMethod(),
180+
$profile->getUrl(),
181+
$profile->getTime(),
182+
$profile->getParentToken(),
183+
)) . "\n";
184+
185+
return $this->appendValue($indexName, $indexRow, $this->lifetime);
186+
}
187+
188+
return false;
189+
}
190+
191+
/**
192+
* Internal convenience method that returns the instance of Redis
193+
*
194+
* @return Redis
195+
*/
196+
protected function getRedis()
197+
{
198+
if (null === $this->redis) {
199+
if (!preg_match('#^redis://(?(?=\[.*\])\[(.*)\]|(.*)):(.*)$#', $this->dsn, $matches)) {
200+
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');
201+
}
202+
203+
$host = $matches[1]?: $matches[2];
204+
$port = $matches[3];
205+
206+
if (!extension_loaded('redis')) {
207+
throw new \RuntimeException('RedisProfilerStorage requires redis extension to be loaded.');
208+
}
209+
210+
$redis = new Redis;
211+
$redis->connect($host, $port);
212+
213+
$redis->setOption(Redis::OPT_PREFIX, self::TOKEN_PREFIX);
214+
215+
$this->redis = $redis;
216+
}
217+
218+
return $this->redis;
219+
}
220+
221+
private function createProfileFromData($token, $data, $parent = null)
222+
{
223+
$profile = new Profile($token);
224+
$profile->setIp($data['ip']);
225+
$profile->setMethod($data['method']);
226+
$profile->setUrl($data['url']);
227+
$profile->setTime($data['time']);
228+
$profile->setCollectors($data['data']);
229+
230+
if (!$parent && $data['parent']) {
231+
$parent = $this->read($data['parent']);
232+
}
233+
234+
if ($parent) {
235+
$profile->setParent($parent);
236+
}
237+
238+
foreach ($data['children'] as $token) {
239+
if (!$token) {
240+
continue;
241+
}
242+
243+
if (!$childProfileData = $this->getValue($this->getItemName($token), Redis::SERIALIZER_PHP)) {
244+
continue;
245+
}
246+
247+
$profile->addChild($this->createProfileFromData($token, $childProfileData, $profile));
248+
}
249+
250+
return $profile;
251+
}
252+
253+
/**
254+
* Get item name
255+
*
256+
* @param string $token
257+
*
258+
* @return string
259+
*/
260+
private function getItemName($token)
261+
{
262+
$name = $token;
263+
264+
if ($this->isItemNameValid($name)) {
265+
return $name;
266+
}
267+
268+
return false;
269+
}
270+
271+
/**
272+
* Get name of index
273+
*
274+
* @return string
275+
*/
276+
private function getIndexName()
277+
{
278+
$name = 'index';
279+
280+
if ($this->isItemNameValid($name)) {
281+
return $name;
282+
}
283+
284+
return false;
285+
}
286+
287+
private function isItemNameValid($name)
288+
{
289+
$length = strlen($name);
290+
291+
if ($length > 2147483648) {
292+
throw new \RuntimeException(sprintf('The Redis item key "%s" is too long (%s bytes). Allowed maximum size is 2^31 bytes.', $name, $length));
293+
}
294+
295+
return true;
296+
}
297+
298+
/**
299+
* Retrieve item from the Redis server
300+
*
301+
* @param string $key
302+
* @param int $serializer
303+
*
304+
* @return mixed
305+
*/
306+
private function getValue($key, $serializer = Redis::SERIALIZER_NONE)
307+
{
308+
$redis = $this->getRedis();
309+
$redis->setOption(Redis::OPT_SERIALIZER, $serializer);
310+
311+
return $redis->get($key);
312+
}
313+
314+
/**
315+
* Store an item on the Redis server under the specified key
316+
*
317+
* @param string $key
318+
* @param mixed $value
319+
* @param int $expiration
320+
* @param int $serializer
321+
*
322+
* @return boolean
323+
*/
324+
private function setValue($key, $value, $expiration = 0, $serializer = Redis::SERIALIZER_NONE)
325+
{
326+
$redis = $this->getRedis();
327+
$redis->setOption(Redis::OPT_SERIALIZER, $serializer);
328+
329+
return $redis->setex($key, $expiration, $value);
330+
}
331+
332+
/**
333+
* Append data to an existing item on the Redis server
334+
*
335+
* @param string $key
336+
* @param string $value
337+
* @param int $expiration
338+
*
339+
* @return boolean
340+
*/
341+
private function appendValue($key, $value, $expiration = 0)
342+
{
343+
$redis = $this->getRedis();
344+
$redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
345+
346+
if ($redis->exists($key)) {
347+
$redis->append($key, $value);
348+
return $redis->setTimeout($key, $expiration);
349+
}
350+
351+
return $redis->setex($key, $expiration, $value);
352+
}
353+
354+
/**
355+
* Remove specified keys
356+
*
357+
* @param array $key
358+
*
359+
* @return boolean
360+
*/
361+
private function delete(array $keys)
362+
{
363+
return (bool) $this->getRedis()->delete($keys);
364+
}
365+
}

0 commit comments

Comments
 (0)