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 {
timestamp int64
Time string `json:"time"`
Timestamp int64 `json:"timestamp"`
Server string `json:"server"`
Cmd string `json:"cmd"`
Cost int64 `json:"cost"`
}
type connectionService struct {
@ -260,17 +260,17 @@ func (c *connectionService) getRedisClient(connName string, db int) (*redis.Clie
ReadTimeout: 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()
last := strings.LastIndex(cmd, ":")
if last != -1 {
cmd = cmd[:last]
}
//last := strings.LastIndex(cmd, ":")
//if last != -1 {
// cmd = cmd[:last]
//}
c.cmdHistory = append(c.cmdHistory, cmdHistoryItem{
timestamp: now.UnixMilli(),
Time: now.Format("2006-01-02 15:04:05"),
Timestamp: now.UnixMilli(),
Server: connName,
Cmd: cmd,
Cost: cost,
})
}))
@ -287,7 +287,7 @@ func (c *connectionService) getRedisClient(connName string, db int) (*redis.Clie
}
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
}
}

View File

@ -2,12 +2,15 @@ package redis
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
"log"
"net"
"strconv"
"time"
)
type execCallback func(string)
type execCallback func(string, int64)
type LogHook struct {
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 {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
return next(ctx, network, addr)
}
}
func (l *LogHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
return func(ctx context.Context, cmd redis.Cmder) error {
log.Println(cmd)
t := time.Now()
err := next(ctx, cmd)
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 {
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 {
log.Println("pipeline: ", cmd)
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",
"version": "0.0.0",
"dependencies": {
"dayjs": "^1.11.9",
"highlight.js": "^11.8.0",
"lodash": "^4.17.21",
"pinia": "^2.1.4",
@ -963,6 +964,11 @@
"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": {
"version": "4.3.4",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz",
@ -2694,6 +2700,11 @@
"dev": true,
"requires": {}
},
"dayjs": {
"version": "1.11.9",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.9.tgz",
"integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz",

View File

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

View File

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

View File

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

View File

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

View File

@ -7,11 +7,19 @@ export const types = {
}
export const typesColor = {
[types.STRING]: '#5A96E3',
[types.HASH]: '#9575DE',
[types.LIST]: '#7A9D54',
[types.SET]: '#F3AA60',
[types.ZSET]: '#FF6666',
[types.STRING]: '#8256DC',
[types.HASH]: '#2983ED',
[types.LIST]: '#26A15E',
[types.SET]: '#EE9F33',
[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]))

View File

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

View File

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

View File

@ -4,6 +4,12 @@ import { createI18n } from 'vue-i18n'
import App from './App.vue'
import { lang } from './langs'
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)
app.use(

View File

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