perf: display command cost time in log list table

perf: beautify color of redis type tag
This commit is contained in:
tiny-craft 2023-07-18 23:43:31 +08:00
parent 76679496c6
commit ab4c78c3d7
12 changed files with 160 additions and 34 deletions

View File

@ -17,10 +17,10 @@ import (
) )
type cmdHistoryItem struct { type cmdHistoryItem struct {
timestamp int64 Timestamp int64 `json:"timestamp"`
Time string `json:"time"`
Server string `json:"server"` Server string `json:"server"`
Cmd string `json:"cmd"` Cmd string `json:"cmd"`
Cost int64 `json:"cost"`
} }
type connectionService struct { type connectionService struct {
@ -260,17 +260,17 @@ func (c *connectionService) getRedisClient(connName string, db int) (*redis.Clie
ReadTimeout: time.Duration(selConn.ExecTimeout) * time.Second, ReadTimeout: time.Duration(selConn.ExecTimeout) * time.Second,
WriteTimeout: time.Duration(selConn.ExecTimeout) * time.Second, WriteTimeout: time.Duration(selConn.ExecTimeout) * time.Second,
}) })
rdb.AddHook(redis2.NewHook(connName, func(cmd string) { rdb.AddHook(redis2.NewHook(connName, func(cmd string, cost int64) {
now := time.Now() now := time.Now()
last := strings.LastIndex(cmd, ":") //last := strings.LastIndex(cmd, ":")
if last != -1 { //if last != -1 {
cmd = cmd[:last] // cmd = cmd[:last]
} //}
c.cmdHistory = append(c.cmdHistory, cmdHistoryItem{ c.cmdHistory = append(c.cmdHistory, cmdHistoryItem{
timestamp: now.UnixMilli(), Timestamp: now.UnixMilli(),
Time: now.Format("2006-01-02 15:04:05"),
Server: connName, Server: connName,
Cmd: cmd, Cmd: cmd,
Cost: cost,
}) })
})) }))
@ -287,7 +287,7 @@ func (c *connectionService) getRedisClient(connName string, db int) (*redis.Clie
} }
if db >= 0 { if db >= 0 {
if err := rdb.Do(ctx, "SELECT", strconv.Itoa(db)).Err(); err != nil { if err := rdb.Do(ctx, "select", strconv.Itoa(db)).Err(); err != nil {
return nil, nil, err return nil, nil, err
} }
} }

View File

@ -2,12 +2,15 @@ package redis
import ( import (
"context" "context"
"fmt"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"log" "log"
"net" "net"
"strconv"
"time"
) )
type execCallback func(string) type execCallback func(string, int64)
type LogHook struct { type LogHook struct {
name string name string
@ -21,28 +24,93 @@ func NewHook(name string, cmdExec execCallback) *LogHook {
} }
} }
func appendArg(b []byte, v interface{}) []byte {
switch v := v.(type) {
case nil:
return append(b, "<nil>"...)
case string:
return append(b, []byte(v)...)
case []byte:
return append(b, v...)
case int:
return strconv.AppendInt(b, int64(v), 10)
case int8:
return strconv.AppendInt(b, int64(v), 10)
case int16:
return strconv.AppendInt(b, int64(v), 10)
case int32:
return strconv.AppendInt(b, int64(v), 10)
case int64:
return strconv.AppendInt(b, v, 10)
case uint:
return strconv.AppendUint(b, uint64(v), 10)
case uint8:
return strconv.AppendUint(b, uint64(v), 10)
case uint16:
return strconv.AppendUint(b, uint64(v), 10)
case uint32:
return strconv.AppendUint(b, uint64(v), 10)
case uint64:
return strconv.AppendUint(b, v, 10)
case float32:
return strconv.AppendFloat(b, float64(v), 'f', -1, 64)
case float64:
return strconv.AppendFloat(b, v, 'f', -1, 64)
case bool:
if v {
return append(b, "true"...)
}
return append(b, "false"...)
case time.Time:
return v.AppendFormat(b, time.RFC3339Nano)
default:
return append(b, fmt.Sprint(v)...)
}
}
func (l *LogHook) DialHook(next redis.DialHook) redis.DialHook { func (l *LogHook) DialHook(next redis.DialHook) redis.DialHook {
return func(ctx context.Context, network, addr string) (net.Conn, error) { return func(ctx context.Context, network, addr string) (net.Conn, error) {
return next(ctx, network, addr) return next(ctx, network, addr)
} }
} }
func (l *LogHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { func (l *LogHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
return func(ctx context.Context, cmd redis.Cmder) error { return func(ctx context.Context, cmd redis.Cmder) error {
log.Println(cmd) log.Println(cmd)
t := time.Now()
err := next(ctx, cmd)
if l.cmdExec != nil { if l.cmdExec != nil {
l.cmdExec(cmd.String()) b := make([]byte, 0, 64)
for i, arg := range cmd.Args() {
if i > 0 {
b = append(b, ' ')
} }
return next(ctx, cmd) b = appendArg(b, arg)
}
l.cmdExec(string(b), time.Since(t).Milliseconds())
}
return err
} }
} }
func (l *LogHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { func (l *LogHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
return func(ctx context.Context, cmds []redis.Cmder) error { return func(ctx context.Context, cmds []redis.Cmder) error {
t := time.Now()
err := next(ctx, cmds)
cost := time.Since(t).Milliseconds()
for _, cmd := range cmds { for _, cmd := range cmds {
log.Println("pipeline: ", cmd) log.Println("pipeline: ", cmd)
if l.cmdExec != nil { if l.cmdExec != nil {
l.cmdExec(cmd.String()) b := make([]byte, 0, 64)
for i, arg := range cmd.Args() {
if i > 0 {
b = append(b, ' ')
}
b = appendArg(b, arg)
}
l.cmdExec(string(b), cost)
} }
} }
return next(ctx, cmds) return err
} }
} }

View File

@ -8,6 +8,7 @@
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"dayjs": "^1.11.9",
"highlight.js": "^11.8.0", "highlight.js": "^11.8.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"pinia": "^2.1.4", "pinia": "^2.1.4",
@ -963,6 +964,11 @@
"date-fns": ">=2.0.0" "date-fns": ">=2.0.0"
} }
}, },
"node_modules/dayjs": {
"version": "1.11.9",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.9.tgz",
"integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz",
@ -2694,6 +2700,11 @@
"dev": true, "dev": true,
"requires": {} "requires": {}
}, },
"dayjs": {
"version": "1.11.9",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.9.tgz",
"integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
},
"debug": { "debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz",

View File

@ -9,6 +9,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"dayjs": "^1.11.9",
"highlight.js": "^11.8.0", "highlight.js": "^11.8.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"pinia": "^2.1.4", "pinia": "^2.1.4",

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import { typesColor, validType } from '../../consts/support_redis_type.js' import { typesBgColor, typesColor, validType } from '../../consts/support_redis_type.js'
const props = defineProps({ const props = defineProps({
type: { type: {
@ -10,22 +10,23 @@ const props = defineProps({
}, },
default: 'STRING', default: 'STRING',
}, },
color: { bordered: Boolean,
type: String,
default: 'white',
},
size: String, size: String,
}) })
const backgroundColor = computed(() => { const fontColor = computed(() => {
return typesColor[props.type] return typesColor[props.type]
}) })
const backgroundColor = computed(() => {
return typesBgColor[props.type]
})
</script> </script>
<template> <template>
<n-tag <n-tag
:bordered="false" :bordered="props.bordered"
:color="{ color: backgroundColor, textColor: props.color }" :color="{ color: backgroundColor, borderColor: fontColor, textColor: fontColor }"
:size="props.size" :size="props.size"
:class="[props.size === 'small' ? 'redis-type-tag-small' : 'redis-type-tag']" :class="[props.size === 'small' ? 'redis-type-tag-small' : 'redis-type-tag']"
strong strong

View File

@ -5,6 +5,7 @@ import Refresh from '../icons/Refresh.vue'
import useConnectionStore from '../../stores/connections.js' import useConnectionStore from '../../stores/connections.js'
import { map, uniqBy } from 'lodash' import { map, uniqBy } from 'lodash'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import dayjs from 'dayjs'
const connectionStore = useConnectionStore() const connectionStore = useConnectionStore()
const i18n = useI18n() const i18n = useI18n()
@ -75,10 +76,15 @@ onActivated(() => {
:columns="[ :columns="[
{ {
title: $t('exec_time'), title: $t('exec_time'),
key: 'time', key: 'timestamp',
defaultSortOrder: 'ascend', defaultSortOrder: 'ascend',
sorter: 'default', sorter: 'default',
width: 180, width: 180,
align: 'center',
titleAlign: 'center',
render({ timestamp }, index) {
return dayjs(timestamp).locale('zh-cn').format('YYYY-MM-DD hh:mm:ss')
},
}, },
{ {
title: $t('server'), title: $t('server'),
@ -88,16 +94,35 @@ onActivated(() => {
return value === '' || row.server === value.toString() return value === '' || row.server === value.toString()
}, },
width: 150, width: 150,
align: 'center',
titleAlign: 'center',
ellipsis: true, ellipsis: true,
}, },
{ {
title: $t('cmd'), title: $t('cmd'),
key: 'cmd', key: 'cmd',
titleAlign: 'center',
filterOptionValue: data.keyword, filterOptionValue: data.keyword,
resizable: true,
filter(value, row) { filter(value, row) {
return value === '' || !!~row.cmd.indexOf(value.toString()) return value === '' || !!~row.cmd.indexOf(value.toString())
}, },
}, },
{
title: $t('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`
}
},
},
]" ]"
:data="data.history" :data="data.history"
flex-height flex-height

View File

@ -20,7 +20,7 @@ import ToggleServer from '../icons/ToggleServer.vue'
import Unlink from '../icons/Unlink.vue' import Unlink from '../icons/Unlink.vue'
import Filter from '../icons/Filter.vue' import Filter from '../icons/Filter.vue'
import Close from '../icons/Close.vue' import Close from '../icons/Close.vue'
import { typesColor } from '../../consts/support_redis_type.js' import { typesBgColor, typesColor } from '../../consts/support_redis_type.js'
const props = defineProps({ const props = defineProps({
server: String, server: String,
@ -299,7 +299,10 @@ const renderSuffix = ({ option }) => {
size: 'small', size: 'small',
closable: true, closable: true,
bordered: false, bordered: false,
color: { color: typesColor[typeFilter], textColor: 'white' }, color: {
color: typesBgColor[typeFilter],
textColor: typesColor[typeFilter],
},
onClose: () => { onClose: () => {
// remove type filter // remove type filter
connectionStore.setKeyFilter(server, db, matchPattern) connectionStore.setKeyFilter(server, db, matchPattern)

View File

@ -7,11 +7,19 @@ export const types = {
} }
export const typesColor = { export const typesColor = {
[types.STRING]: '#5A96E3', [types.STRING]: '#8256DC',
[types.HASH]: '#9575DE', [types.HASH]: '#2983ED',
[types.LIST]: '#7A9D54', [types.LIST]: '#26A15E',
[types.SET]: '#F3AA60', [types.SET]: '#EE9F33',
[types.ZSET]: '#FF6666', [types.ZSET]: '#CE3352',
}
export const typesBgColor = {
[types.STRING]: '#F2EDFB',
[types.HASH]: '#E4F0FC',
[types.LIST]: '#E3F3EB',
[types.SET]: '#FDF1DF',
[types.ZSET]: '#FAEAED',
} }
// export const typesName = Object.fromEntries(Object.entries(types).map(([key, value]) => [key, value.name])) // export const typesName = Object.fromEntries(Object.entries(types).map(([key, value]) => [key, value.name]))

View File

@ -153,5 +153,6 @@
"filter_server": "Filter Server", "filter_server": "Filter Server",
"filter_keyword": "Filter Keyword", "filter_keyword": "Filter Keyword",
"exec_time": "Exec Time", "exec_time": "Exec Time",
"cmd": "Command" "cmd": "Command",
"cost_time": "Cost"
} }

View File

@ -155,5 +155,6 @@
"filter_server": "筛选服务器", "filter_server": "筛选服务器",
"filter_keyword": "筛选关键字", "filter_keyword": "筛选关键字",
"exec_time": "执行时间", "exec_time": "执行时间",
"cmd": "命令" "cmd": "命令",
"cost_time": "耗时"
} }

View File

@ -4,6 +4,12 @@ import { createI18n } from 'vue-i18n'
import App from './App.vue' import App from './App.vue'
import { lang } from './langs' import { lang } from './langs'
import './style.scss' import './style.scss'
import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration'
import relativeTime from 'dayjs/plugin/relativeTime'
dayjs.extend(duration)
dayjs.extend(relativeTime)
const app = createApp(App) const app = createApp(App)
app.use( app.use(

View File

@ -73,6 +73,7 @@ const useConnectionStore = defineStore('connections', {
* @property {string} time * @property {string} time
* @property {string} server * @property {string} server
* @property {string} cmd * @property {string} cmd
* @property {number} cost
*/ */
/** /**
@ -167,7 +168,7 @@ const useConnectionStore = defineStore('connections', {
/** /**
* get connection by name from local profile * get connection by name from local profile
* @param name * @param name
* @returns {Promise<{}|null>} * @returns {Promise<ConnectionProfile|null>}
*/ */
async getConnectionProfile(name) { async getConnectionProfile(name) {
try { try {