Skip to content

Commit 5c7753c

Browse files
committed
Add redis command calls metrics
1 parent c39f812 commit 5c7753c

File tree

7 files changed

+161
-58
lines changed

7 files changed

+161
-58
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
- View, add, edit, and delete keys. Supports all Redis data types.
2828
- **Cluster support**.
2929
- Supports ACL.
30-
- Detailed server statistics including memory usage, uptime, connected clients, and general info.
30+
- Detailed server statistics including command calls, memory usage, uptime, connected clients, and general info.
3131
- View the Redis slowlog to debug performance issues.
3232
- Supports both SCAN and KEYS commands for retrieving keys.
3333

assets/js/scripts.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,7 @@ const chart = (instance, options, timestamps) => {
474474
grid: {left: 10, right: 10, top: 80, bottom: 60}
475475
});
476476
};
477+
477478
const time_switcher = (callback) => {
478479
const time_buttons = document.querySelectorAll('[data-tab]');
479480

src/Dashboards/Redis/Compatibility/Cluster/PredisCluster.php

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -97,25 +97,26 @@ public function getInfo(?string $option = null, ?array $combine = null): array {
9797
foreach ($this->getInfoSections() as $section_name) {
9898
try {
9999
$response = $node->info($section_name);
100-
101100
$node_section_info = (is_array($response) && $response !== []) ? reset($response) : null;
102-
if (!$node_section_info) {
103-
continue;
104-
}
105101

106-
if (!is_array($node_section_info)) {
102+
if (!$node_section_info || !is_array($node_section_info)) {
107103
continue;
108104
}
109105

110-
$section_name_lower = strtolower($section_name);
106+
$section_lower = strtolower($section_name);
111107

112108
foreach ($node_section_info as $key => $value) {
109+
if ($section_lower === 'commandstats' || $section_lower === 'keyspace') {
110+
$aggregated[$section_lower][$key][] = $value;
111+
continue;
112+
}
113+
113114
if (is_array($value)) {
114115
foreach ($value as $sub_key => $sub_val) {
115-
$aggregated[$section_name_lower][$key][$sub_key][] = $sub_val;
116+
$aggregated[$section_lower][$key][$sub_key][] = $sub_val;
116117
}
117118
} else {
118-
$aggregated[$section_name_lower][$key][] = $value;
119+
$aggregated[$section_lower][$key][] = $value;
119120
}
120121
}
121122
} catch (Exception) {
@@ -124,21 +125,7 @@ public function getInfo(?string $option = null, ?array $combine = null): array {
124125
}
125126
}
126127

127-
$combined_info = [];
128-
129-
foreach ($aggregated as $section_name => $section_data) {
130-
foreach ($section_data as $key => $values) {
131-
if (is_array(reset($values))) {
132-
foreach ($values as $sub_key => $sub_values) {
133-
$combined_info[$section_name][$key][$sub_key] = $this->combineValues($sub_key, $sub_values, $combine);
134-
}
135-
} else {
136-
$combined_info[$section_name][$key] = $this->combineValues($key, $values, $combine);
137-
}
138-
}
139-
}
140-
141-
$info = $combined_info;
128+
$info = $this->aggregatedData($aggregated, $combine);
142129

143130
return $option !== null ? ($info[strtolower($option)] ?? []) : $info;
144131
}

src/Dashboards/Redis/Compatibility/Cluster/RedisCluster.php

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public function getInfo(?string $option = null, ?array $combine = null): array {
8888
return $option !== null ? ($info[strtolower($option)] ?? []) : $info;
8989
}
9090

91-
$aggregated_data = [];
91+
$aggregated = [];
9292

9393
foreach ($this->nodes as $node) {
9494
foreach ($this->getInfoSections() as $section_name) {
@@ -99,13 +99,20 @@ public function getInfo(?string $option = null, ?array $combine = null): array {
9999
continue;
100100
}
101101

102+
$section_lower = strtolower($section_name);
103+
102104
foreach ($node_section_info as $key => $value) {
105+
if ($section_lower === 'commandstats' || $section_lower === 'keyspace') {
106+
$aggregated[$section_lower][$key][] = $value;
107+
continue;
108+
}
109+
103110
if (is_array($value)) {
104111
foreach ($value as $sub_key => $sub_val) {
105-
$aggregated_data[strtolower($section_name)][$key][$sub_key][] = $sub_val;
112+
$aggregated[$section_lower][$key][$sub_key][] = $sub_val;
106113
}
107114
} else {
108-
$aggregated_data[strtolower($section_name)][$key][] = $value;
115+
$aggregated[$section_lower][$key][] = $value;
109116
}
110117
}
111118
} catch (RedisClusterException) {
@@ -114,25 +121,7 @@ public function getInfo(?string $option = null, ?array $combine = null): array {
114121
}
115122
}
116123

117-
$combined_info = [];
118-
119-
foreach ($aggregated_data as $section_name => $section_data) {
120-
$combined_section = [];
121-
122-
foreach ($section_data as $key => $values) {
123-
if (is_array(reset($values))) {
124-
foreach ($values as $sub_key => $sub_values) {
125-
$combined_section[$key][$sub_key] = $this->combineValues((string) $sub_key, $sub_values, $combine);
126-
}
127-
} else {
128-
$combined_section[$key] = $this->combineValues($key, $values, $combine);
129-
}
130-
}
131-
132-
$combined_info[$section_name] = $combined_section;
133-
}
134-
135-
$info = $combined_info;
124+
$info = $this->aggregatedData($aggregated, $combine);
136125

137126
return $option !== null ? ($info[strtolower($option)] ?? []) : $info;
138127
}

src/Dashboards/Redis/Compatibility/RedisExtra.php

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,24 @@ public function getInfoSections(): array {
2525
* @return array<string, array<string, mixed>>
2626
*/
2727
public function parseSectionData(string $section): array {
28-
/** @var array<string, string> $info */
28+
/** @var array<string, string|array<int, string>> $info */
2929
$info = $this->getInfo($section);
3030

31-
return array_map(static function (string $value): array {
32-
parse_str(str_replace(',', '&', $value), $parsed);
31+
return array_map(static function ($value): array {
32+
// Cluster mode
33+
if (is_array($value)) {
34+
$aggregated = [];
35+
foreach ($value as $node_string) {
36+
parse_str(str_replace(',', '&', (string) $node_string), $parsed);
37+
foreach ($parsed as $k => $v) {
38+
$aggregated[$k] = ($aggregated[$k] ?? 0) + (is_numeric($v) ? $v : 0);
39+
}
40+
}
41+
42+
return $aggregated;
43+
}
44+
45+
parse_str(str_replace(',', '&', (string) $value), $parsed);
3346

3447
return $parsed;
3548
}, $info);
@@ -87,7 +100,38 @@ public function checkModule(string $module): bool {
87100
}
88101

89102
/**
90-
* Combine info values into a single value in a cluster.
103+
* Helper function for cluster mode.
104+
*
105+
* @param array<string, array<string, mixed>> $aggregated
106+
* @param null|list<string> $combine
107+
*
108+
* @return array<string, array<string, mixed>>
109+
*/
110+
private function aggregatedData(array $aggregated, ?array $combine = null): array {
111+
$combined_info = [];
112+
113+
foreach ($aggregated as $section_name => $section_data) {
114+
foreach ($section_data as $key => $values) {
115+
if ($section_name === 'commandstats' || $section_name === 'keyspace') {
116+
$combined_info[$section_name][$key] = $values;
117+
continue;
118+
}
119+
120+
if (is_array(reset($values))) {
121+
foreach ($values as $sub_key => $sub_values) {
122+
$combined_info[$section_name][$key][$sub_key] = $this->combineValues((string) $sub_key, $sub_values, $combine);
123+
}
124+
} else {
125+
$combined_info[$section_name][$key] = $this->combineValues($key, $values, $combine);
126+
}
127+
}
128+
}
129+
130+
return $combined_info;
131+
}
132+
133+
/**
134+
* Helper function for cluster mode.
91135
*
92136
* @param list<mixed> $values
93137
* @param list<string>|null $combine

src/Dashboards/Redis/RedisMetrics.php

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,45 @@ public function __construct(
4141

4242
$schema = <<<SQL
4343
CREATE TABLE IF NOT EXISTS metrics (
44-
id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER NOT NULL,
45-
commands_per_second INTEGER, hit_rate REAL, memory_used INTEGER, memory_peak INTEGER,
46-
fragmentation_ratio REAL, connections INTEGER
44+
id INTEGER PRIMARY KEY AUTOINCREMENT,
45+
timestamp INTEGER NOT NULL,
46+
commands_per_second INTEGER,
47+
hit_rate REAL,
48+
memory_used INTEGER,
49+
memory_peak INTEGER,
50+
fragmentation_ratio REAL,
51+
connections INTEGER,
52+
commands_stats TEXT
4753
)
4854
SQL;
4955

5056
$this->pdo->exec($schema);
57+
58+
$this->updateSchema([
59+
'commands_stats' => 'TEXT',
60+
]);
61+
}
62+
63+
/**
64+
* @param array<string, string> $new_columns
65+
*/
66+
private function updateSchema(array $new_columns): void {
67+
try {
68+
$statement = $this->pdo->query('PRAGMA table_info(metrics)');
69+
$existing_columns = $statement->fetchAll(PDO::FETCH_COLUMN, 1);
70+
71+
foreach ($new_columns as $column_name => $type) {
72+
if (!in_array($column_name, $existing_columns, true)) {
73+
$this->pdo->exec(sprintf('ALTER TABLE metrics ADD COLUMN %s %s', $column_name, $type));
74+
}
75+
}
76+
} catch (\Exception) {
77+
}
5178
}
5279

80+
/**
81+
* @throws JsonException
82+
*/
5383
public function collectAndRespond(): string {
5484
$info = $this->redis->getInfo(null, [
5585
'used_memory',
@@ -79,13 +109,23 @@ public function collectAndRespond(): string {
79109
/**
80110
* @param array<string, mixed> $info
81111
*
82-
* @return array<string, int|float>
112+
* @return array<string, int|float|string>
113+
*
114+
* @throws JsonException
83115
*/
84116
private function calculateMetrics(array $info): array {
85117
$keyspace_hits = $info['stats']['keyspace_hits'] ?? 0;
86118
$keyspace_misses = $info['stats']['keyspace_misses'] ?? 0;
87119
$total_commands = $keyspace_hits + $keyspace_misses;
88120

121+
$parsed_commands = $this->redis->parseSectionData('commandstats');
122+
$command_calls = [];
123+
124+
foreach ($parsed_commands as $cmd => $details) {
125+
$name = str_replace('cmdstat_', '', $cmd);
126+
$command_calls[$name] = (int) ($details['calls'] ?? 0);
127+
}
128+
89129
return [
90130
'timestamp' => time(),
91131
'commands_per_second' => $info['stats']['instantaneous_ops_per_sec'] ?? 0,
@@ -94,6 +134,7 @@ private function calculateMetrics(array $info): array {
94134
'memory_peak' => $info['memory']['used_memory_peak'] ?? 0,
95135
'fragmentation_ratio' => $info['memory']['mem_fragmentation_ratio'] ?? 0,
96136
'connections' => $info['clients']['connected_clients'] ?? 0,
137+
'commands_stats' => json_encode($command_calls, JSON_THROW_ON_ERROR),
97138
];
98139
}
99140

@@ -119,15 +160,15 @@ private function fetchRecentMetrics(): array {
119160
$stmt->bindValue(':limit', $max_data_points_to_return, PDO::PARAM_INT);
120161
$stmt->execute();
121162

122-
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
123-
124-
return array_reverse($results);
163+
return array_reverse($stmt->fetchAll(PDO::FETCH_ASSOC));
125164
}
126165

127166
/**
128167
* @param array<int, array<string, mixed>> $db_rows
129168
*
130169
* @return array<int, array<string, mixed>>
170+
*
171+
* @throws JsonException
131172
*/
132173
private function formatDataForResponse(array $db_rows): array {
133174
$formatted_results = [];
@@ -144,6 +185,7 @@ private function formatDataForResponse(array $db_rows): array {
144185
'fragmentation' => $row['fragmentation_ratio'],
145186
],
146187
'connections' => $row['connections'],
188+
'commands_stats' => json_decode((string) $row['commands_stats'], true, 512, JSON_THROW_ON_ERROR),
147189
];
148190
}
149191

templates/dashboards/redis/metrics.twig

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
}) }}
1212

1313
<div class="gap-4 md:grid md:grid-cols-4">
14+
<div class="col-span-4 p-4 mb-2 bg-white rounded-md border border-gray-200 dark:bg-gray-800 dark:border-gray-700">
15+
<div id="command_calls_chart" class="h-90"></div>
16+
</div>
1417
<div class="col-span-2 p-4 mb-2 bg-white rounded-md border border-gray-200 dark:bg-gray-800 dark:border-gray-700">
1518
<div id="memory_chart" class="h-90"></div>
1619
</div>
@@ -34,6 +37,7 @@
3437
const initial_theme = document.documentElement.classList.contains('dark') ? 'dark' : null;
3538
3639
const chart_config = {
40+
command_calls: echarts.init(document.getElementById('command_calls_chart'), initial_theme, {renderer: 'svg'}),
3741
memory: echarts.init(document.getElementById('memory_chart'), initial_theme, {renderer: 'svg'}),
3842
fragmentation: echarts.init(document.getElementById('fragmentation_chart'), initial_theme, {renderer: 'svg'}),
3943
commands: echarts.init(document.getElementById('commands_chart'), initial_theme, {renderer: 'svg'}),
@@ -45,6 +49,43 @@
4549
if (!data || data.length === 0) return;
4650
const timestamps = data.map(p => p.timestamp.split(' ')[1]);
4751
52+
const command_series = [...new Set(data.flatMap(p => Object.keys(p.commands_stats ?? {})))].map(name => ({
53+
name: name,
54+
type: 'line',
55+
stack: 'Total',
56+
areaStyle: {},
57+
emphasis: {focus: 'series'},
58+
data: data.map((p, i) => {
59+
if (i === 0 || !p.commands_stats || !data[i - 1].commands_stats) return 0;
60+
const val = (p.commands_stats?.[name] ?? 0) - (data[i - 1].commands_stats?.[name] ?? 0);
61+
return val < 0 ? 0 : val;
62+
})
63+
})).filter(s => s.data.some(v => v > 0));
64+
65+
chart(chart_config.command_calls, {
66+
title: 'Command Calls',
67+
legend: command_series.map(s => s.name),
68+
tooltip: {
69+
confine: true,
70+
formatter: function (params) {
71+
let html = `<div style="min-width: 450px;">
72+
<strong>${params[0].name}</strong>
73+
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0 20px;">`;
74+
75+
params.forEach(item => {
76+
if (item.value > 0) {
77+
html += `<div class="flex justify-between items-center"><span>${item.marker} ${item.seriesName}</span><strong>${item.value}</strong></div>`;
78+
}
79+
});
80+
81+
html += '</div></div>';
82+
return html;
83+
}
84+
},
85+
yAxis: {type: 'value', name: 'Calls'},
86+
series: command_series
87+
}, timestamps);
88+
4889
chart(chart_config.memory, {
4990
title: 'Memory Usage',
5091
legend: ['Used', 'Peak'],
@@ -91,7 +132,6 @@
91132
{name: 'Connections', type: 'line', data: data.map(p => p.connections), areaStyle: {}},
92133
],
93134
}, timestamps);
94-
95135
};
96136
97137
init_metrics(render_charts, chart_config);

0 commit comments

Comments
 (0)