mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-30 17:05:19 +08:00
feat: Redis/Valkey 添加慢日志/客户端/内存诊断/大Key扫描
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -102,12 +102,12 @@ func initAce() (*app.Ace, func(), error) {
|
||||
postgresqlApp := postgresql.NewApp(locale, config, databaseServerRepo, settingRepo, taskRepo)
|
||||
prometheusApp := prometheus.NewApp(config, locale, taskRepo)
|
||||
pureftpdApp := pureftpd.NewApp(locale)
|
||||
redisApp := redis.NewApp(locale, databaseServerRepo)
|
||||
redisApp := redis.NewApp(locale, databaseServerRepo, taskRepo)
|
||||
rocketmqApp := rocketmq.NewApp(locale)
|
||||
rsyncApp := rsync.NewApp(locale)
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
valkeyApp := valkey.NewApp(locale, databaseServerRepo)
|
||||
valkeyApp := valkey.NewApp(locale, databaseServerRepo, taskRepo)
|
||||
loader := bootstrap.NewLoader(apacheApp, clickhouseApp, codeserverApp, dockerApp, elasticsearchApp, fail2banApp, frpApp, giteaApp, grafanaApp, kafkaApp, mariadbApp, memcachedApp, minioApp, mongodbApp, mysqlApp, nginxApp, openrestyApp, opensearchApp, perconaApp, pgadminApp, phpmyadminApp, podmanApp, postgresqlApp, prometheusApp, pureftpdApp, redisApp, rocketmqApp, rsyncApp, s3fsApp, supervisorApp, valkeyApp)
|
||||
manager, err := bootstrap.NewSession(config, db, slogLogger)
|
||||
if err != nil {
|
||||
|
||||
+260
-3
@@ -1,41 +1,50 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
redigo "github.com/gomodule/redigo/redis"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/tools"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
taskRepo biz.TaskRepo
|
||||
slug string // 服务名与配置目录名,如 redis、valkey
|
||||
name string // 展示名,如 Redis、Valkey
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo) *App {
|
||||
return New("redis", "Redis", t, databaseServerRepo)
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, taskRepo biz.TaskRepo) *App {
|
||||
return New("redis", "Redis", t, databaseServerRepo, taskRepo)
|
||||
}
|
||||
|
||||
func New(slug, name string, t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo) *App {
|
||||
func New(slug, name string, t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, taskRepo biz.TaskRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
taskRepo: taskRepo,
|
||||
slug: slug,
|
||||
name: name,
|
||||
}
|
||||
@@ -47,6 +56,13 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
// 性能诊断
|
||||
r.Get("/slow_log", s.SlowLog)
|
||||
r.Post("/slow_log/reset", s.ResetSlowLog)
|
||||
r.Get("/clients", s.ClientList)
|
||||
r.Post("/clients/kill", s.KillClient)
|
||||
r.Get("/memory", s.MemoryStatus)
|
||||
r.Post("/bigkeys", s.ScanBigKeys)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
@@ -185,6 +201,247 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// SlowLog 获取慢日志
|
||||
func (s *App) SlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
reply, err := conn.Exec("SLOWLOG", "GET", 50)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
rows, err := redigo.Values(reply, nil)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
entries := make([]SlowLogEntry, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
item, itemErr := redigo.Values(row, nil)
|
||||
if itemErr != nil || len(item) < 4 {
|
||||
continue
|
||||
}
|
||||
entry := SlowLogEntry{}
|
||||
entry.ID, _ = redigo.Int64(item[0], nil)
|
||||
if ts, tsErr := redigo.Int64(item[1], nil); tsErr == nil {
|
||||
entry.Time = time.Unix(ts, 0).Format(time.DateTime)
|
||||
}
|
||||
entry.DurationUs, _ = redigo.Int64(item[2], nil)
|
||||
if cmd, cmdErr := redigo.Strings(item[3], nil); cmdErr == nil {
|
||||
entry.Command = strings.Join(cmd, " ")
|
||||
}
|
||||
if len(item) > 4 {
|
||||
entry.Client, _ = redigo.String(item[4], nil)
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
|
||||
service.Success(w, entries)
|
||||
}
|
||||
|
||||
// ResetSlowLog 重置慢日志
|
||||
func (s *App) ResetSlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if _, err = conn.Exec("SLOWLOG", "RESET"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ClientList 获取客户端连接列表
|
||||
func (s *App) ClientList(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
raw, err := redigo.String(conn.Exec("CLIENT", "LIST"))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
clients := make([]Client, 0)
|
||||
for line := range strings.SplitSeq(strings.TrimSpace(raw), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
fields := map[string]string{}
|
||||
for kv := range strings.FieldsSeq(line) {
|
||||
if key, value, found := strings.Cut(kv, "="); found {
|
||||
fields[key] = value
|
||||
}
|
||||
}
|
||||
clients = append(clients, Client{
|
||||
ID: fields["id"],
|
||||
Addr: fields["addr"],
|
||||
Name: fields["name"],
|
||||
DB: fields["db"],
|
||||
Age: fields["age"],
|
||||
Idle: fields["idle"],
|
||||
Cmd: fields["cmd"],
|
||||
})
|
||||
}
|
||||
|
||||
service.Success(w, clients)
|
||||
}
|
||||
|
||||
// KillClient 踢除客户端连接
|
||||
func (s *App) KillClient(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ClientKill](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
killed, err := redigo.Int64(conn.Exec("CLIENT", "KILL", "ID", req.ID))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if killed == 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("client %d not found", req.ID))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// MemoryStatus 获取内存诊断信息
|
||||
func (s *App) MemoryStatus(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
memory := Memory{Items: make([]types.NV, 0)}
|
||||
memory.Doctor, err = redigo.String(conn.Exec("MEMORY", "DOCTOR"))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
reply, err := conn.Exec("MEMORY", "STATS")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
values, err := redigo.Values(reply, nil)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
stats := map[string]string{}
|
||||
for i := 0; i+1 < len(values); i += 2 {
|
||||
key, keyErr := redigo.String(values[i], nil)
|
||||
if keyErr != nil {
|
||||
continue
|
||||
}
|
||||
// 值可能为整数或字符串,嵌套数组(如 db.0)跳过
|
||||
if v, vErr := redigo.Int64(values[i+1], nil); vErr == nil {
|
||||
stats[key] = cast.ToString(v)
|
||||
continue
|
||||
}
|
||||
if v, vErr := redigo.String(values[i+1], nil); vErr == nil {
|
||||
stats[key] = v
|
||||
}
|
||||
}
|
||||
|
||||
// 各版本键名存在差异,仅展示存在的指标
|
||||
items := []struct {
|
||||
key string
|
||||
name string
|
||||
bytes bool
|
||||
}{
|
||||
{"peak.allocated", s.t.Get("Peak Allocated"), true},
|
||||
{"total.allocated", s.t.Get("Total Allocated"), true},
|
||||
{"startup.allocated", s.t.Get("Startup Allocated"), true},
|
||||
{"dataset.bytes", s.t.Get("Dataset Size"), true},
|
||||
{"dataset.percentage", s.t.Get("Dataset Percentage"), false},
|
||||
{"keys.count", s.t.Get("Keys Count"), false},
|
||||
{"keys.bytes-per-key", s.t.Get("Bytes Per Key"), true},
|
||||
{"allocator-fragmentation.ratio", s.t.Get("Allocator Fragmentation Ratio"), false},
|
||||
{"fragmentation", s.t.Get("Fragmentation Ratio"), false},
|
||||
}
|
||||
for _, item := range items {
|
||||
value, ok := stats[item.key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if item.bytes {
|
||||
value = tools.FormatBytes(cast.ToFloat64(value))
|
||||
}
|
||||
memory.Items = append(memory.Items, types.NV{Name: item.name, Value: value})
|
||||
}
|
||||
|
||||
service.Success(w, memory)
|
||||
}
|
||||
|
||||
// ScanBigKeys 扫描大 Key(异步任务)
|
||||
func (s *App) ScanBigKeys(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(s.confPath())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
withPassword := ""
|
||||
if password := confval.Directive.Get(config, "requirepass"); password != "" {
|
||||
withPassword = " -a " + password
|
||||
}
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Key = s.slug + ":bigkeys"
|
||||
task.Name = s.t.Get("Scan %s big keys", s.name)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = fmt.Sprintf("%s-cli%s --bigkeys", s.slug, withPassword)
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// connect 从配置文件读取端口与密码建立连接
|
||||
func (s *App) connect(ctx context.Context) (*db.Redis, error) {
|
||||
config, err := io.Read(s.confPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
port := confval.Directive.Get(config, "port")
|
||||
if port == "" {
|
||||
port = "6379"
|
||||
}
|
||||
password := confval.Directive.Get(config, "requirepass")
|
||||
|
||||
return db.NewRedis(ctx, "", password, "127.0.0.1:"+port)
|
||||
}
|
||||
|
||||
func (s *App) confPath() string {
|
||||
return filepath.Join(app.Root, "server", s.slug, s.slug+".conf")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package redis
|
||||
|
||||
import "github.com/acepanel/panel/v3/pkg/types"
|
||||
|
||||
// ConfigTune Redis 协议兼容服务的配置调整
|
||||
type ConfigTune struct {
|
||||
// 常规设置
|
||||
@@ -16,3 +18,34 @@ type ConfigTune struct {
|
||||
Appendonly string `form:"appendonly" json:"appendonly" validate:"in:yes,no"`
|
||||
Appendfsync string `form:"appendfsync" json:"appendfsync" validate:"in:always,everysec,no"`
|
||||
}
|
||||
|
||||
// ClientKill 踢除客户端连接请求
|
||||
type ClientKill struct {
|
||||
ID int64 `form:"id" json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
// SlowLogEntry 慢日志条目
|
||||
type SlowLogEntry struct {
|
||||
ID int64 `json:"id"`
|
||||
Time string `json:"time"`
|
||||
DurationUs int64 `json:"duration_us"`
|
||||
Command string `json:"command"`
|
||||
Client string `json:"client"`
|
||||
}
|
||||
|
||||
// Client 客户端连接信息
|
||||
type Client struct {
|
||||
ID string `json:"id"`
|
||||
Addr string `json:"addr"`
|
||||
Name string `json:"name"`
|
||||
DB string `json:"db"`
|
||||
Age string `json:"age"`
|
||||
Idle string `json:"idle"`
|
||||
Cmd string `json:"cmd"`
|
||||
}
|
||||
|
||||
// Memory 内存诊断信息
|
||||
type Memory struct {
|
||||
Doctor string `json:"doctor"`
|
||||
Items []types.NV `json:"items"`
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ type App struct {
|
||||
redis *redis.App
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo) *App {
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, taskRepo biz.TaskRepo) *App {
|
||||
return &App{
|
||||
redis: redis.New("valkey", "Valkey", t, databaseServerRepo),
|
||||
redis: redis.New("valkey", "Valkey", t, databaseServerRepo, taskRepo),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,4 +11,14 @@ export default {
|
||||
configTune: (): any => http.Get('/apps/redis/config_tune'),
|
||||
// 保存配置调整参数
|
||||
saveConfigTune: (data: any): any => http.Post('/apps/redis/config_tune', data),
|
||||
// 慢日志
|
||||
slowLog: (): any => http.Get('/apps/redis/slow_log'),
|
||||
resetSlowLog: (): any => http.Post('/apps/redis/slow_log/reset'),
|
||||
// 客户端连接
|
||||
clients: (): any => http.Get('/apps/redis/clients'),
|
||||
killClient: (id: number): any => http.Post('/apps/redis/clients/kill', { id }),
|
||||
// 内存诊断
|
||||
memory: (): any => http.Get('/apps/redis/memory'),
|
||||
// 扫描大 Key
|
||||
scanBigKeys: (): any => http.Post('/apps/redis/bigkeys'),
|
||||
}
|
||||
|
||||
@@ -6,4 +6,14 @@ export default {
|
||||
saveConfig: (config: string): any => http.Post('/apps/valkey/config', { config }),
|
||||
configTune: (): any => http.Get('/apps/valkey/config_tune'),
|
||||
saveConfigTune: (data: any): any => http.Post('/apps/valkey/config_tune', data),
|
||||
// 慢日志
|
||||
slowLog: (): any => http.Get('/apps/valkey/slow_log'),
|
||||
resetSlowLog: (): any => http.Post('/apps/valkey/slow_log/reset'),
|
||||
// 客户端连接
|
||||
clients: (): any => http.Get('/apps/valkey/clients'),
|
||||
killClient: (id: number): any => http.Post('/apps/valkey/clients/kill', { id }),
|
||||
// 内存诊断
|
||||
memory: (): any => http.Get('/apps/valkey/memory'),
|
||||
// 扫描大 Key
|
||||
scanBigKeys: (): any => http.Post('/apps/valkey/bigkeys'),
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import redis from '@/api/apps/redis'
|
||||
import ServiceStatus from '@/components/common/ServiceStatus.vue'
|
||||
|
||||
import RedisConfigTuneView from './RedisConfigTuneView.vue'
|
||||
import RedisPerformanceView from '@/views/apps/redis/RedisPerformanceView.vue'
|
||||
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const currentTab = ref('status')
|
||||
@@ -87,6 +89,9 @@ const handleSaveConfig = () => {
|
||||
<n-tab-pane name="config-tune" :tab="$gettext('Parameter Tuning')">
|
||||
<redis-config-tune-view />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="performance" :tab="$gettext('Performance')">
|
||||
<redis-performance-view :api="redis" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" :tab="$gettext('Load Status')">
|
||||
<n-data-table
|
||||
striped
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import type redis from '@/api/apps/redis'
|
||||
import { useConfirm } from '@/components/system/composables/useConfirm'
|
||||
|
||||
const props = defineProps<{
|
||||
api: typeof redis
|
||||
}>()
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const { confirmDelete, confirmAction } = useConfirm()
|
||||
|
||||
const { data: slowLog, send: refreshSlowLog } = useRequest(props.api.slowLog, {
|
||||
initialData: [],
|
||||
})
|
||||
const { data: clients, send: refreshClients } = useRequest(props.api.clients, {
|
||||
initialData: [],
|
||||
})
|
||||
const { data: memory, send: refreshMemory } = useRequest(props.api.memory, {
|
||||
initialData: { doctor: '', items: [] },
|
||||
})
|
||||
|
||||
const slowLogColumns: any = [
|
||||
{ title: 'ID', key: 'id', width: 90 },
|
||||
{ title: $gettext('Time'), key: 'time', width: 180 },
|
||||
{
|
||||
title: $gettext('Duration (ms)'),
|
||||
key: 'duration_us',
|
||||
width: 130,
|
||||
render: (row: any) => Math.round(row.duration_us / 10) / 100,
|
||||
},
|
||||
{ title: $gettext('Command'), key: 'command', minWidth: 300, ellipsis: { tooltip: true } },
|
||||
{ title: $gettext('Client'), key: 'client', width: 160, ellipsis: { tooltip: true } },
|
||||
]
|
||||
|
||||
const clientColumns: any = [
|
||||
{ title: 'ID', key: 'id', width: 90 },
|
||||
{ title: $gettext('Address'), key: 'addr', width: 170, ellipsis: { tooltip: true } },
|
||||
{ title: $gettext('Name'), key: 'name', width: 120, ellipsis: { tooltip: true } },
|
||||
{ title: $gettext('Database'), key: 'db', width: 90 },
|
||||
{ title: $gettext('Age (s)'), key: 'age', width: 90 },
|
||||
{ title: $gettext('Idle (s)'), key: 'idle', width: 90 },
|
||||
{ title: $gettext('Command'), key: 'cmd', minWidth: 150, ellipsis: { tooltip: true } },
|
||||
{
|
||||
title: $gettext('Actions'),
|
||||
key: 'actions',
|
||||
width: 110,
|
||||
render(row: any) {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
onClick: async () => {
|
||||
const ok = await confirmDelete({
|
||||
title: $gettext('Confirm Kill'),
|
||||
content: $gettext('Are you sure you want to kill connection %{ addr }?', {
|
||||
addr: row.addr,
|
||||
}),
|
||||
positiveText: $gettext('Kill'),
|
||||
})
|
||||
if (ok) handleKillClient(Number(row.id))
|
||||
},
|
||||
},
|
||||
{ default: () => $gettext('Kill') },
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const memoryColumns: any = [
|
||||
{ title: $gettext('Property'), key: 'name', minWidth: 200, ellipsis: { tooltip: true } },
|
||||
{ title: $gettext('Current Value'), key: 'value', minWidth: 200, ellipsis: { tooltip: true } },
|
||||
]
|
||||
|
||||
const handleResetSlowLog = async () => {
|
||||
const ok = await confirmAction({
|
||||
type: 'warning',
|
||||
title: $gettext('Confirm Reset'),
|
||||
content: $gettext('Are you sure you want to reset the slow log?'),
|
||||
})
|
||||
if (!ok) return
|
||||
useRequest(props.api.resetSlowLog()).onSuccess(() => {
|
||||
window.$message.success($gettext('Reset successfully'))
|
||||
refreshSlowLog()
|
||||
})
|
||||
}
|
||||
|
||||
const handleKillClient = (id: number) => {
|
||||
useRequest(props.api.killClient(id)).onSuccess(() => {
|
||||
window.$message.success($gettext('Killed successfully'))
|
||||
refreshClients()
|
||||
})
|
||||
}
|
||||
|
||||
const handleScanBigKeys = async () => {
|
||||
const ok = await confirmAction({
|
||||
type: 'info',
|
||||
title: $gettext('Confirm Scan'),
|
||||
content: $gettext(
|
||||
'Scanning traverses all keys and may take a while on large datasets. The result will be in the task log.',
|
||||
),
|
||||
})
|
||||
if (!ok) return
|
||||
useRequest(props.api.scanBigKeys()).onSuccess(() => {
|
||||
window.$message.success($gettext('Task submitted, please check progress in background tasks'))
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-tabs type="segment" animated>
|
||||
<n-tab-pane name="slow-log" :tab="$gettext('Slow Log')">
|
||||
<n-flex vertical>
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="() => refreshSlowLog()">
|
||||
{{ $gettext('Refresh') }}
|
||||
</n-button>
|
||||
<n-button type="warning" @click="handleResetSlowLog">
|
||||
{{ $gettext('Reset Slow Log') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-data-table
|
||||
striped
|
||||
:columns="slowLogColumns"
|
||||
:data="slowLog"
|
||||
:scroll-x="860"
|
||||
max-height="60vh"
|
||||
/>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="clients" :tab="$gettext('Clients')">
|
||||
<n-flex vertical>
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="() => refreshClients()">
|
||||
{{ $gettext('Refresh') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-data-table
|
||||
striped
|
||||
:columns="clientColumns"
|
||||
:data="clients"
|
||||
:scroll-x="920"
|
||||
max-height="60vh"
|
||||
/>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="memory" :tab="$gettext('Memory')">
|
||||
<n-flex vertical>
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="() => refreshMemory()">
|
||||
{{ $gettext('Refresh') }}
|
||||
</n-button>
|
||||
<n-button type="info" @click="handleScanBigKeys">
|
||||
{{ $gettext('Scan Big Keys') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-alert v-if="memory.doctor" type="info">
|
||||
{{ memory.doctor }}
|
||||
</n-alert>
|
||||
<n-data-table striped :columns="memoryColumns" :data="memory.items" :scroll-x="400" />
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</template>
|
||||
@@ -10,6 +10,8 @@ import valkey from '@/api/apps/valkey'
|
||||
import ServiceStatus from '@/components/common/ServiceStatus.vue'
|
||||
|
||||
import ValkeyConfigTuneView from './ValkeyConfigTuneView.vue'
|
||||
import RedisPerformanceView from '@/views/apps/redis/RedisPerformanceView.vue'
|
||||
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const currentTab = ref('status')
|
||||
@@ -87,6 +89,9 @@ const handleSaveConfig = () => {
|
||||
<n-tab-pane name="config-tune" :tab="$gettext('Parameter Tuning')">
|
||||
<valkey-config-tune-view />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="performance" :tab="$gettext('Performance')">
|
||||
<redis-performance-view :api="valkey" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" :tab="$gettext('Load Status')">
|
||||
<n-data-table
|
||||
striped
|
||||
|
||||
Reference in New Issue
Block a user