Skip to content

Commit b3bd8a9

Browse files
committed
Add Memcached commands stats
1 parent 977f1f3 commit b3bd8a9

File tree

4 files changed

+155
-42
lines changed

4 files changed

+155
-42
lines changed

src/Dashboards/Memcached/MemcachedDashboard.php

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,6 @@ class MemcachedDashboard implements DashboardInterface {
2727

2828
public PHPMem $memcached;
2929

30-
/**
31-
* @var array<int, mixed>
32-
*/
33-
private array $all_keys = [];
34-
3530
public function __construct(private readonly Template $template) {
3631
$this->servers = Config::get('memcached', []);
3732

@@ -115,7 +110,6 @@ public function dashboard(): string {
115110

116111
try {
117112
$this->memcached = $this->connect($this->servers[$this->current_server]);
118-
$this->all_keys = $this->memcached->getKeys();
119113
$select = Helpers::serverSelector($this->template, $this->servers, $this->current_server);
120114

121115
$this->template->addGlobal('side', $select.$this->panels());

src/Dashboards/Memcached/MemcachedTrait.php

Lines changed: 115 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,7 @@ private function panels(): string {
2424
try {
2525
$info = $this->memcached->getServerStats();
2626

27-
$bytes = (int) $info['bytes'];
28-
$max_bytes = (int) $info['limit_maxbytes'];
29-
$get_hits = (int) $info['get_hits'];
30-
$get_misses = (int) $info['get_misses'];
31-
$memory_usage = round(($bytes / $max_bytes) * 100, 2);
32-
$hit_rate = $get_hits !== 0 ? round(($get_hits / ($get_hits + $get_misses)) * 100, 2) : 0;
27+
$memory_usage = round(($info['bytes'] / $info['limit_maxbytes']) * 100, 2);
3328

3429
$panels = [
3530
[
@@ -38,31 +33,34 @@ private function panels(): string {
3833
'data' => [
3934
'Version' => $info['version'],
4035
'Open connections' => $info['curr_connections'],
41-
'Uptime' => Format::seconds((int) $info['uptime']),
36+
'Uptime' => Format::seconds($info['uptime']),
4237
],
4338
],
4439
[
4540
'title' => 'Memory',
4641
'data' => [
47-
'Total' => Format::bytes($max_bytes, 0),
48-
['Used', Format::bytes($bytes).' ('.$memory_usage.'%)', $memory_usage],
49-
'Free' => Format::bytes($max_bytes - $bytes),
42+
'Total' => Format::bytes($info['limit_maxbytes'], 0),
43+
['Used', Format::bytes($info['bytes']).' ('.$memory_usage.'%)', $memory_usage],
44+
'Free' => Format::bytes($info['limit_maxbytes'] - $info['bytes']),
5045
],
5146
],
5247
[
53-
'title' => 'Stats',
48+
'title' => 'Keys',
5449
'data' => [
55-
'Keys' => Format::number(count($this->all_keys)),
56-
['Hits / Misses', Format::number($get_hits).' / '.Format::number($get_misses).' (Rate '.$hit_rate.'%)', $hit_rate, 'higher'],
57-
'Evictions' => Format::number((int) $info['evictions']),
50+
'Current' => Format::number($info['curr_items']),
51+
'Total (since start)' => Format::number($info['total_items']),
52+
'Evictions' => Format::number($info['evictions']),
53+
'Reclaimed' => Format::number($info['reclaimed']),
54+
'Expired Unfetched' => Format::number($info['expired_unfetched']),
55+
'Evicted Unfetched' => Format::number($info['evicted_unfetched']),
5856
],
5957
],
6058
[
6159
'title' => 'Connections',
6260
'data' => [
63-
'Current' => Format::number((int) $info['curr_connections']).' / '.Format::number((int) $info['max_connections']),
64-
'Total' => Format::number((int) $info['total_connections']),
65-
'Rejected' => Format::number((int) $info['rejected_connections']),
61+
'Current' => Format::number($info['curr_connections']).' / '.Format::number($info['max_connections']).' max',
62+
'Total' => Format::number($info['total_connections']),
63+
'Rejected' => Format::number($info['rejected_connections']),
6664
],
6765
],
6866
];
@@ -88,6 +86,10 @@ private function moreInfo(): string {
8886
try {
8987
$info = $this->memcached->getServerStats();
9088

89+
foreach (['settings', 'sizes', 'conns'] as $type) {
90+
$info += [$type => $this->memcached->getServerStats($type)];
91+
}
92+
9193
if (extension_loaded('memcached') || extension_loaded('memcache')) {
9294
$memcached = extension_loaded('memcached') ? 'd' : '';
9395
$info += Helpers::getExtIniInfo('memcache'.$memcached);
@@ -199,6 +201,7 @@ private function form(): string {
199201

200202
/**
201203
* @return array<int, array<string, string|int>>
204+
* @throws MemcachedException
202205
*/
203206
private function getAllKeys(): array {
204207
static $keys = [];
@@ -208,7 +211,9 @@ private function getAllKeys(): array {
208211

209212
$time = time();
210213

211-
foreach ($this->all_keys as $key_data) {
214+
$all_keys = $this->memcached->getKeys();
215+
216+
foreach ($all_keys as $key_data) {
212217
$key_data = $this->memcached->parseLine($key_data);
213218

214219
$key = $key_data['key'];
@@ -233,19 +238,108 @@ private function getAllKeys(): array {
233238
return $keys;
234239
}
235240

241+
private function commandsStats(): string {
242+
try {
243+
$info = $this->memcached->getServerStats();
244+
245+
$rate = static function (int $hits, int $total): float {
246+
return $hits !== 0 ? round(($hits / $total) * 100, 2) : 0;
247+
};
248+
249+
$get_hit_rate = $rate($info['get_hits'], $info['cmd_get']);
250+
$delete_hit_rate = $rate($info['delete_hits'], $info['delete_hits'] + $info['delete_misses']);
251+
$incr_hit_rate = $rate($info['incr_hits'], $info['incr_hits'] + $info['incr_misses']);
252+
$decr_hit_rate = $rate($info['decr_hits'], $info['decr_hits'] + $info['decr_misses']);
253+
$cas_hit_rate = $rate($info['cas_hits'], $info['cas_hits'] + $info['cas_misses']);
254+
$touch_hit_rate = $rate($info['touch_hits'], $info['cmd_touch']);
255+
256+
$commands = [
257+
[
258+
'title' => 'get',
259+
'data' => [
260+
'Hits' => Format::number($info['get_hits']),
261+
'Misses' => Format::number($info['get_misses']),
262+
['Hit Rate', $get_hit_rate.'%', $get_hit_rate, 'higher'],
263+
],
264+
],
265+
[
266+
'title' => 'delete',
267+
'data' => [
268+
'Hits' => Format::number($info['delete_hits']),
269+
'Misses' => Format::number($info['delete_misses']),
270+
['Hit Rate', $delete_hit_rate.'%', $delete_hit_rate, 'higher'],
271+
],
272+
],
273+
[
274+
'title' => 'incr',
275+
'data' => [
276+
'Hits' => Format::number($info['incr_hits']),
277+
'Misses' => Format::number($info['incr_misses']),
278+
['Hit Rate', $incr_hit_rate.'%', $incr_hit_rate, 'higher'],
279+
],
280+
],
281+
[
282+
'title' => 'decr',
283+
'data' => [
284+
'Hits' => Format::number($info['decr_hits']),
285+
'Misses' => Format::number($info['decr_misses']),
286+
['Hit Rate', $decr_hit_rate.'%', $decr_hit_rate, 'higher'],
287+
],
288+
],
289+
[
290+
'title' => 'touch',
291+
'data' => [
292+
'Hits' => Format::number($info['touch_hits']),
293+
'Misses' => Format::number($info['touch_misses']),
294+
['Hit Rate', $touch_hit_rate.'%', $touch_hit_rate, 'higher'],
295+
],
296+
],
297+
[
298+
'title' => 'cas',
299+
'data' => [
300+
'Hits' => Format::number($info['cas_hits']),
301+
'Misses' => Format::number($info['cas_misses']),
302+
['Hit Rate', $cas_hit_rate.'%', $cas_hit_rate, 'higher'],
303+
'Bad Value' => $info['cas_badval'],
304+
],
305+
],
306+
[
307+
'title' => 'set',
308+
'data' => [
309+
'Total' => Format::number($info['cmd_set']),
310+
],
311+
],
312+
[
313+
'title' => 'flush',
314+
'data' => [
315+
'Total' => Format::number($info['cmd_flush']),
316+
],
317+
],
318+
];
319+
} catch (MemcachedException $e) {
320+
$commands = ['error' => $e->getMessage()];
321+
}
322+
323+
return $this->template->render('dashboards/memcached', ['commands' => $commands]);
324+
}
325+
236326
/**
237327
* @throws MemcachedException
238328
*/
239329
private function mainDashboard(): string {
240-
$keys = $this->getAllKeys();
241-
242330
if (isset($_POST['submit_import_key'])) {
243331
Helpers::import(
244332
fn (string $key): bool => $this->memcached->exists($key),
245333
fn (string $key, string $value, int $ttl): bool => $this->memcached->set($key, base64_decode($value), $ttl)
246334
);
247335
}
248336

337+
if (Http::get('tab') === 'commands_stats') {
338+
return $this->commandsStats();
339+
}
340+
341+
$keys = $this->getAllKeys();
342+
249343
if (isset($_GET['export_btn'])) {
250344
Helpers::export($keys, 'memcached_backup', fn (string $key): string => base64_encode($this->memcached->getKey($key)));
251345
}
@@ -254,7 +348,7 @@ private function mainDashboard(): string {
254348

255349
return $this->template->render('dashboards/memcached', [
256350
'keys' => $paginator->getPaginated(),
257-
'all_keys' => count($this->all_keys),
351+
'all_keys' => count($keys),
258352
'paginator' => $paginator->render(),
259353
'view_key' => Http::queryString([], ['view' => 'key', 'key' => '__key__']),
260354
]);

src/Dashboards/Memcached/PHPMem.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,15 @@ public function flush(): bool {
8080
*
8181
* @throws MemcachedException
8282
*/
83-
public function getServerStats(): array {
84-
$raw = $this->runCommand('stats');
83+
public function getServerStats(?string $type = null): array {
84+
$type = in_array($type, ['settings', 'items', 'sizes', 'slabs', 'conns'], true) ? ' '.$type : '';
85+
$raw = $this->runCommand('stats'.$type);
8586
$stats = [];
8687

8788
foreach (explode("\r\n", $raw) as $line) {
8889
if (str_starts_with($line, 'STAT')) {
8990
[, $key, $value] = explode(' ', $line, 3);
90-
$stats[$key] = $value;
91+
$stats[$key] = is_numeric($value) ? (int) $value : $value;
9192
}
9293
}
9394

@@ -194,7 +195,8 @@ public function exists(string $key): bool {
194195
* touch <key> <ttl>
195196
* delete <key>
196197
* incr|decr <key> <value>
197-
* stats [items|slabs|sizes|cachedump <slab_id> <limit>|reset|conns]
198+
* stats <settings|items|sizes|slabs|conns>
199+
* stats cachedump <slab_id> <limit>\r\n
198200
* flush_all
199201
* version
200202
* lru <tune|mode|temp_ttl> <option list>
Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,36 @@
1-
{{ include('partials/keys_table.twig', {
2-
buttons: {
3-
import_btn: true,
4-
export_btn: true,
5-
add_new_btn: true,
1+
{{ include('components/tabs.twig', {
2+
links: {
3+
'keys': 'Keys',
4+
'commands_stats': 'Commands Stats',
65
},
7-
head_items: [
8-
{'title': 'Key', 'sort': 'link_title'},
9-
{'title': 'Size', 'class': 'w-24', 'sort': 'bytes_size'},
10-
{'title': 'Last used', 'class': 'w-32', 'sort': 'timediff_last_access'},
11-
{'title': 'TTL', 'class': 'w-32', 'sort': 'ttl'},
12-
],
136
}) }}
7+
8+
{% if get('tab', 'keys') == 'keys' %}
9+
{{ include('partials/keys_table.twig', {
10+
tabs: true,
11+
buttons: {
12+
import_btn: true,
13+
export_btn: true,
14+
add_new_btn: true,
15+
},
16+
head_items: [
17+
{'title': 'Key', 'sort': 'link_title'},
18+
{'title': 'Size', 'class': 'w-24', 'sort': 'bytes_size'},
19+
{'title': 'Last used', 'class': 'w-32', 'sort': 'timediff_last_access'},
20+
{'title': 'TTL', 'class': 'w-32', 'sort': 'ttl'},
21+
],
22+
}) }}
23+
{% endif %}
24+
25+
{% if get('tab') == 'commands_stats' %}
26+
<div class="px-6 py-4 rounded-b bg-white border border-gray-200 dark:bg-gray-800 dark:border-gray-700">
27+
<div class="md:grid md:grid-cols-3 md:gap-4">
28+
{% for command in commands %}
29+
{{ include('partials/panel.twig', {
30+
panel_title: command.title,
31+
array: command.data,
32+
}) }}
33+
{% endfor %}
34+
</div>
35+
</div>
36+
{% endif %}

0 commit comments

Comments
 (0)