feat: add slow log
This commit is contained in:
parent
9b9d0e3c7c
commit
b95f293185
|
@ -11,6 +11,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -33,6 +34,14 @@ type cmdHistoryItem struct {
|
||||||
Cost int64 `json:"cost"`
|
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 {
|
type connectionService struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
conns *ConnectionsStorage
|
conns *ConnectionsStorage
|
||||||
|
@ -1613,6 +1622,59 @@ func (c *connectionService) CleanCmdHistory() (resp types.JSResp) {
|
||||||
return
|
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
|
// update or insert key info to database
|
||||||
//func (c *connectionService) updateDBKey(connName string, db int, keys []string, separator string) {
|
//func (c *connectionService) updateDBKey(connName string, db int, keys []string, separator string) {
|
||||||
// dbStruct := map[string]any{}
|
// dbStruct := map[string]any{}
|
||||||
|
|
|
@ -74,7 +74,7 @@ defineExpose({
|
||||||
<n-card
|
<n-card
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
:theme-overrides="{ borderRadius: '0px' }"
|
:theme-overrides="{ borderRadius: '0px' }"
|
||||||
:title="$t('log.launch_log')"
|
:title="$t('log.title')"
|
||||||
class="content-container flex-box-v"
|
class="content-container flex-box-v"
|
||||||
content-style="display: flex;flex-direction: column; overflow: hidden; backgroundColor: gray">
|
content-style="display: flex;flex-direction: column; overflow: hidden; backgroundColor: gray">
|
||||||
<n-form :disabled="data.loading" class="flex-item" inline>
|
<n-form :disabled="data.loading" class="flex-item" inline>
|
||||||
|
|
|
@ -13,6 +13,7 @@ import ContentValueWrapper from '@/components/content_value/ContentValueWrapper.
|
||||||
import ContentCli from '@/components/content_value/ContentCli.vue'
|
import ContentCli from '@/components/content_value/ContentCli.vue'
|
||||||
import Monitor from '@/components/icons/Monitor.vue'
|
import Monitor from '@/components/icons/Monitor.vue'
|
||||||
import Pub from '@/components/icons/Pub.vue'
|
import Pub from '@/components/icons/Pub.vue'
|
||||||
|
import ContentSlog from '@/components/content_value/ContentSlog.vue'
|
||||||
|
|
||||||
const themeVars = useThemeVars()
|
const themeVars = useThemeVars()
|
||||||
|
|
||||||
|
@ -164,7 +165,7 @@ watch(
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
<!-- slow log 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>
|
<template #tab>
|
||||||
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
|
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
|
||||||
<n-icon size="16">
|
<n-icon size="16">
|
||||||
|
@ -176,10 +177,11 @@ watch(
|
||||||
<span>{{ $t('interface.sub_tab.slow_log') }}</span>
|
<span>{{ $t('interface.sub_tab.slow_log') }}</span>
|
||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
|
<content-slog :db="tabContent.db" :server="props.server" />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
<!-- command monitor 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>
|
<template #tab>
|
||||||
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
|
<n-space :size="5" :wrap-item="false" align="center" inline justify="center">
|
||||||
<n-icon size="16">
|
<n-icon size="16">
|
||||||
|
|
|
@ -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=" ">
|
||||||
|
<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>
|
|
@ -274,7 +274,7 @@
|
||||||
"about": "About"
|
"about": "About"
|
||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"launch_log": "Launch Log",
|
"title": "Launch Log",
|
||||||
"filter_server": "Filter Server",
|
"filter_server": "Filter Server",
|
||||||
"filter_keyword": "Filter Keyword",
|
"filter_keyword": "Filter Keyword",
|
||||||
"clean_log": "Clean Launch Log",
|
"clean_log": "Clean Launch Log",
|
||||||
|
@ -293,5 +293,16 @@
|
||||||
"all_info": "Information",
|
"all_info": "Information",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"auto_refresh": "Auto 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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -273,7 +273,7 @@
|
||||||
"about": "关于"
|
"about": "关于"
|
||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"launch_log": "运行日志",
|
"title": "运行日志",
|
||||||
"filter_server": "筛选服务器",
|
"filter_server": "筛选服务器",
|
||||||
"filter_keyword": "筛选关键字",
|
"filter_keyword": "筛选关键字",
|
||||||
"clean_log": "清空运行日志",
|
"clean_log": "清空运行日志",
|
||||||
|
@ -292,5 +292,16 @@
|
||||||
"all_info": "全部信息",
|
"all_info": "全部信息",
|
||||||
"refresh": "立即刷新",
|
"refresh": "立即刷新",
|
||||||
"auto_refresh": "自动刷新"
|
"auto_refresh": "自动刷新"
|
||||||
|
},
|
||||||
|
"slog": {
|
||||||
|
"title": "慢日志",
|
||||||
|
"limit": "条数",
|
||||||
|
"filter": "筛选",
|
||||||
|
"exec_time": "执行时间",
|
||||||
|
"client": "客户端",
|
||||||
|
"cmd": "命令",
|
||||||
|
"cost_time": "耗时",
|
||||||
|
"refresh": "立即刷新",
|
||||||
|
"auto_refresh": "自动刷新"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import {
|
||||||
GetCmdHistory,
|
GetCmdHistory,
|
||||||
GetConnection,
|
GetConnection,
|
||||||
GetKeyValue,
|
GetKeyValue,
|
||||||
|
GetSlowLogs,
|
||||||
ListConnection,
|
ListConnection,
|
||||||
LoadAllKeys,
|
LoadAllKeys,
|
||||||
LoadNextKeys,
|
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
|
* get key filter pattern and filter type
|
||||||
* @param {string} server
|
* @param {string} server
|
||||||
|
|
Loading…
Reference in New Issue