feat: add slow log

This commit is contained in:
tiny-craft 2023-11-02 22:28:12 +08:00
parent 9b9d0e3c7c
commit b95f293185
7 changed files with 307 additions and 5 deletions

View File

@ -11,6 +11,7 @@ import (
"net"
"net/url"
"os"
"sort"
"strconv"
"strings"
"sync"
@ -33,6 +34,14 @@ type cmdHistoryItem struct {
Cost int64 `json:"cost"`
}
type slowLogItem struct {
Timestamp int64 `json:"timestamp"`
Client string `json:"client"`
Addr string `json:"addr"`
Cmd string `json:"cmd"`
Cost int64 `json:"cost"`
}
type connectionService struct {
ctx context.Context
conns *ConnectionsStorage
@ -1613,6 +1622,59 @@ func (c *connectionService) CleanCmdHistory() (resp types.JSResp) {
return
}
// GetSlowLogs get slow log list
func (c *connectionService) GetSlowLogs(connName string, db int, num int64) (resp types.JSResp) {
item, err := c.getRedisClient(connName, db)
if err != nil {
resp.Msg = err.Error()
return
}
client, ctx := item.client, item.ctx
var logs []redis.SlowLog
if cluster, ok := client.(*redis.ClusterClient); ok {
// cluster mode
var mu sync.Mutex
err = cluster.ForEachShard(ctx, func(ctx context.Context, cli *redis.Client) error {
if subLogs, _ := client.SlowLogGet(ctx, num).Result(); len(subLogs) > 0 {
mu.Lock()
logs = append(logs, subLogs...)
mu.Unlock()
}
return nil
})
} else {
logs, err = client.SlowLogGet(ctx, num).Result()
}
if err != nil {
resp.Msg = err.Error()
return
}
sort.Slice(logs, func(i, j int) bool {
return logs[i].Time.UnixMilli() > logs[j].Time.UnixMilli()
})
if len(logs) > int(num) {
logs = logs[:num]
}
list := sliceutil.Map(logs, func(i int) slowLogItem {
return slowLogItem{
Timestamp: logs[i].Time.UnixMilli(),
Client: logs[i].ClientName,
Addr: logs[i].ClientAddr,
Cmd: sliceutil.JoinString(logs[i].Args, " "),
Cost: logs[i].Duration.Milliseconds(),
}
})
resp.Success = true
resp.Data = map[string]any{
"list": list,
}
return
}
// update or insert key info to database
//func (c *connectionService) updateDBKey(connName string, db int, keys []string, separator string) {
// dbStruct := map[string]any{}

View File

@ -74,7 +74,7 @@ defineExpose({
<n-card
:bordered="false"
:theme-overrides="{ borderRadius: '0px' }"
:title="$t('log.launch_log')"
:title="$t('log.title')"
class="content-container flex-box-v"
content-style="display: flex;flex-direction: column; overflow: hidden; backgroundColor: gray">
<n-form :disabled="data.loading" class="flex-item" inline>

View File

@ -13,6 +13,7 @@ import ContentValueWrapper from '@/components/content_value/ContentValueWrapper.
import ContentCli from '@/components/content_value/ContentCli.vue'
import Monitor from '@/components/icons/Monitor.vue'
import Pub from '@/components/icons/Pub.vue'
import ContentSlog from '@/components/content_value/ContentSlog.vue'
const themeVars = useThemeVars()
@ -164,7 +165,7 @@ watch(
</n-tab-pane>
<!-- slow log pane -->
<n-tab-pane :disabled="true" :name="BrowserTabType.SlowLog.toString()">
<n-tab-pane :name="BrowserTabType.SlowLog.toString()" display-directive="if">
<template #tab>
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
<n-icon size="16">
@ -176,10 +177,11 @@ watch(
<span>{{ $t('interface.sub_tab.slow_log') }}</span>
</n-space>
</template>
<content-slog :db="tabContent.db" :server="props.server" />
</n-tab-pane>
<!-- command monitor pane -->
<n-tab-pane :disabled="true" :name="BrowserTabType.CmdMonitor.toString()">
<n-tab-pane :disabled="true" :name="BrowserTabType.CmdMonitor.toString()" display-directive="if">
<template #tab>
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
<n-icon size="16">

View File

@ -0,0 +1,198 @@
<script setup>
import { h, onMounted, onUnmounted, reactive, ref } from 'vue'
import Refresh from '@/components/icons/Refresh.vue'
import useConnectionStore from 'stores/connections.js'
import { debounce, isEmpty, map, size, split } from 'lodash'
import { useI18n } from 'vue-i18n'
import dayjs from 'dayjs'
import { useThemeVars } from 'naive-ui'
const themeVars = useThemeVars()
const connectionStore = useConnectionStore()
const i18n = useI18n()
const props = defineProps({
server: {
type: String,
},
db: {
type: Number,
default: 0,
},
})
const data = reactive({
list: [],
sortOrder: 'descend',
listLimit: 20,
loading: false,
autoLoading: false,
client: '',
keyword: '',
})
const tableRef = ref(null)
const _loadSlowLog = () => {
data.loading = true
connectionStore
.getSlowLog(props.server, props.db, data.listLimit)
.then((list) => {
data.list = list || []
})
.finally(() => {
data.loading = false
tableRef.value?.scrollTo({ top: data.sortOrder === 'ascend' ? 999999 : 0 })
})
}
const loadSlowLog = debounce(_loadSlowLog, 1000, { leading: true, trailing: true })
let intervalID
onMounted(() => {
loadSlowLog()
intervalID = setInterval(() => {
if (data.autoLoading === true) {
loadSlowLog()
}
}, 5000)
})
onUnmounted(() => {
clearInterval(intervalID)
})
const onListLimitChanged = (limit) => {
loadSlowLog()
}
</script>
<template>
<n-card
:bordered="false"
:theme-overrides="{ borderRadius: '0px' }"
:title="$t('slog.title')"
class="content-container flex-box-v"
content-style="display: flex;flex-direction: column; overflow: hidden; backgroundColor: gray">
<n-form :disabled="data.loading" class="flex-item" inline>
<n-form-item :label="$t('slog.limit')">
<n-input-number
v-model:value="data.listLimit"
:max="9999"
style="width: 120px"
@update:value="onListLimitChanged" />
</n-form-item>
<n-form-item :label="$t('slog.auto_refresh')">
<n-switch v-model:value="data.autoLoading" :loading="data.loading" />
</n-form-item>
<n-form-item label="&nbsp;">
<n-tooltip>
{{ $t('slog.refresh') }}
<template #trigger>
<n-button :loading="data.loading" circle size="small" tertiary @click="_loadSlowLog">
<template #icon>
<n-icon :component="Refresh" />
</template>
</n-button>
</template>
</n-tooltip>
</n-form-item>
<n-form-item :label="$t('slog.filter')">
<n-input v-model:value="data.keyword" clearable placeholder="" />
</n-form-item>
</n-form>
<div class="content-value fill-height flex-box-h">
<n-data-table
ref="tableRef"
:columns="[
{
title: $t('slog.exec_time'),
key: 'timestamp',
sortOrder: data.sortOrder,
sorter: 'default',
width: 180,
align: 'center',
titleAlign: 'center',
render({ timestamp }, index) {
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')
},
},
{
title: $t('slog.client'),
key: 'client',
filterOptionValue: data.client,
resizable: true,
filter(value, row) {
return value === '' || row.client === value.toString() || row.addr === value.toString()
},
width: 200,
align: 'center',
titleAlign: 'center',
ellipsis: true,
render({ client, addr }, index) {
let content = ''
if (!isEmpty(client)) {
content += client
}
if (!isEmpty(addr)) {
if (!isEmpty(content)) {
content += ' - '
}
content += addr
}
return content
},
},
{
title: $t('slog.cmd'),
key: 'cmd',
titleAlign: 'center',
filterOptionValue: data.keyword,
resizable: true,
width: 100,
filter(value, row) {
return value === '' || !!~row.cmd.indexOf(value.toString())
},
render({ cmd }, index) {
const cmdList = split(cmd, '\n')
if (size(cmdList) > 1) {
return h(
'div',
null,
map(cmdList, (c) => h('div', null, c)),
)
}
return cmd
},
},
{
title: $t('slog.cost_time'),
key: 'cost',
width: 100,
align: 'center',
titleAlign: 'center',
render({ cost }, index) {
const ms = dayjs.duration(cost).asMilliseconds()
if (ms < 1000) {
return `${ms} ms`
} else {
return `${Math.floor(ms / 1000)} s`
}
},
},
]"
@update:sorter="({ order }) => (data.sortOrder = order)"
:data="data.list"
class="flex-item-expand"
flex-height />
</div>
</n-card>
</template>
<style lang="scss" scoped>
@import '@/styles/content';
.content-container {
padding: 5px;
box-sizing: border-box;
}
</style>

View File

@ -274,7 +274,7 @@
"about": "About"
},
"log": {
"launch_log": "Launch Log",
"title": "Launch Log",
"filter_server": "Filter Server",
"filter_keyword": "Filter Keyword",
"clean_log": "Clean Launch Log",
@ -293,5 +293,16 @@
"all_info": "Information",
"refresh": "Refresh",
"auto_refresh": "Auto Refresh"
},
"slog": {
"title": "Slow Log",
"limit": "Limit",
"filter": "Filter",
"exec_time": "Time",
"client": "Client",
"cmd": "Command",
"cost_time": "Cost",
"refresh": "Refresh Now",
"auto_refresh": "Auto Refresh"
}
}

View File

@ -273,7 +273,7 @@
"about": "关于"
},
"log": {
"launch_log": "运行日志",
"title": "运行日志",
"filter_server": "筛选服务器",
"filter_keyword": "筛选关键字",
"clean_log": "清空运行日志",
@ -292,5 +292,16 @@
"all_info": "全部信息",
"refresh": "立即刷新",
"auto_refresh": "自动刷新"
},
"slog": {
"title": "慢日志",
"limit": "条数",
"filter": "筛选",
"exec_time": "执行时间",
"client": "客户端",
"cmd": "命令",
"cost_time": "耗时",
"refresh": "立即刷新",
"auto_refresh": "自动刷新"
}
}

View File

@ -29,6 +29,7 @@ import {
GetCmdHistory,
GetConnection,
GetKeyValue,
GetSlowLogs,
ListConnection,
LoadAllKeys,
LoadNextKeys,
@ -1744,6 +1745,23 @@ const useConnectionStore = defineStore('connections', {
}
},
/**
* get slow log list
* @param {string} server
* @param {number} db
* @param {number} num
* @return {Promise<[]>}
*/
async getSlowLog(server, db, num) {
try {
const { success, data = { list: [] } } = await GetSlowLogs(server, db, num)
const { list } = data
return list
} catch {
return []
}
},
/**
* get key filter pattern and filter type
* @param {string} server