From cccf5e4a07638b5ad25f2523389b5999e22b418e Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 24 Jun 2023 15:28:11 +0800 Subject: [PATCH] feat: able to query logs now (close #144) --- controller/log.go | 49 ++++- controller/relay-text.go | 11 +- i18n/en.json | 18 +- model/log.go | 109 +++++++++- model/user.go | 5 + router/api-router.go | 2 + web/src/components/LogsTable.js | 339 ++++++++++++++++++++------------ web/src/pages/Log/index.js | 5 +- 8 files changed, 392 insertions(+), 146 deletions(-) diff --git a/controller/log.go b/controller/log.go index 88d2ed1f..09cb1bb6 100644 --- a/controller/log.go +++ b/controller/log.go @@ -13,7 +13,11 @@ func GetAllLogs(c *gin.Context) { p = 0 } logType, _ := strconv.Atoi(c.Query("type")) - logs, err := model.GetAllLogs(logType, p*common.ItemsPerPage, common.ItemsPerPage) + startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) + endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) + username := c.Query("username") + modelName := c.Query("model_name") + logs, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, p*common.ItemsPerPage, common.ItemsPerPage) if err != nil { c.JSON(200, gin.H{ "success": false, @@ -35,7 +39,11 @@ func GetUserLogs(c *gin.Context) { } userId := c.GetInt("id") logType, _ := strconv.Atoi(c.Query("type")) - logs, err := model.GetUserLogs(userId, logType, p*common.ItemsPerPage, common.ItemsPerPage) + startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) + endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) + tokenName := c.Query("token_name") + modelName := c.Query("model_name") + logs, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, p*common.ItemsPerPage, common.ItemsPerPage) if err != nil { c.JSON(200, gin.H{ "success": false, @@ -84,3 +92,40 @@ func SearchUserLogs(c *gin.Context) { "data": logs, }) } + +func GetLogsStat(c *gin.Context) { + logType, _ := strconv.Atoi(c.Query("type")) + startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) + endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) + username := c.Query("username") + modelName := c.Query("model_name") + quotaNum := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, "") + //tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, "") + c.JSON(200, gin.H{ + "success": true, + "message": "", + "data": gin.H{ + "quota": quotaNum, + //"token": tokenNum, + }, + }) +} + +func GetLogsSelfStat(c *gin.Context) { + username := c.GetString("username") + logType, _ := strconv.Atoi(c.Query("type")) + startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) + endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) + tokenName := c.Query("token_name") + modelName := c.Query("model_name") + quotaNum := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName) + //tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, tokenName) + c.JSON(200, gin.H{ + "success": true, + "message": "", + "data": gin.H{ + "quota": quotaNum, + //"token": tokenNum, + }, + }) +} diff --git a/controller/relay-text.go b/controller/relay-text.go index b271092e..26970dc8 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -58,6 +58,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { return err } var promptTokens int + var completionTokens int switch relayMode { case RelayModeChatCompletions: promptTokens = countTokenMessages(textRequest.Messages, textRequest.Model) @@ -128,11 +129,12 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { completionRatio = 2 } if isStream { - responseTokens := countTokenText(streamResponseText, textRequest.Model) - quota = promptTokens + int(float64(responseTokens)*completionRatio) + completionTokens = countTokenText(streamResponseText, textRequest.Model) } else { - quota = textResponse.Usage.PromptTokens + int(float64(textResponse.Usage.CompletionTokens)*completionRatio) + promptTokens = textResponse.Usage.PromptTokens + completionTokens = textResponse.Usage.CompletionTokens } + quota = promptTokens + int(float64(completionTokens)*completionRatio) quota = int(float64(quota) * ratio) if ratio != 0 && quota <= 0 { quota = 1 @@ -143,7 +145,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { common.SysError("error consuming token remain quota: " + err.Error()) } tokenName := c.GetString("token_name") - model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("通过令牌「%s」使用模型 %s 消耗 %s(模型倍率 %.2f,分组倍率 %.2f)", tokenName, textRequest.Model, common.LogQuota(quota), modelRatio, groupRatio)) + logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio) + model.RecordConsumeLog(userId, promptTokens, completionTokens, textRequest.Model, tokenName, quota, logContent) model.UpdateUserUsedQuotaAndRequestCount(userId, quota) channelId := c.GetInt("channel_id") model.UpdateChannelUsedQuota(channelId, quota) diff --git a/i18n/en.json b/i18n/en.json index 32d5b7a4..9072be13 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -441,5 +441,19 @@ "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "This item is read-only. Users need to bind through the relevant binding button on the personal settings page, and cannot be modified directly", "已绑定的微信账户": "WeChat Account Bound", "已绑定的邮箱账户": "Email Account Bound", - "用户信息更新成功!": "User information updated successfully!" -} \ No newline at end of file + "用户信息更新成功!": "User information updated successfully!", + "模型倍率 %.2f,分组倍率 %.2f": "model rate %.2f, group rate %.2f", + "使用明细(总消耗额度:{renderQuota(stat.quota)})": "Usage Details (Total Consumption Quota: {renderQuota(stat.quota)})", + "用户名称": "User Name", + "令牌名称": "Token Name", + "留空则查询全部用户": "Leave blank to query all users", + "留空则查询全部令牌": "Leave blank to query all tokens", + "模型名称": "Model Name", + "留空则查询全部模型": "Leave blank to query all models", + "起始时间": "Start Time", + "结束时间": "End Time", + "查询": "Query", + "提示令牌": "Prompt Token", + "补全令牌": "Completion Token", + "消耗额度": "Used Quota" +} diff --git a/model/log.go b/model/log.go index a21cb6b4..ce56bdbb 100644 --- a/model/log.go +++ b/model/log.go @@ -6,11 +6,17 @@ import ( ) type Log struct { - Id int `json:"id"` - UserId int `json:"user_id" gorm:"index"` - CreatedAt int64 `json:"created_at" gorm:"bigint"` - Type int `json:"type" gorm:"index"` - Content string `json:"content"` + Id int `json:"id"` + UserId int `json:"user_id"` + CreatedAt int64 `json:"created_at" gorm:"bigint;index"` + Type int `json:"type" gorm:"index"` + Content string `json:"content"` + Username string `json:"username" gorm:"index;default:''"` + TokenName string `json:"token_name" gorm:"index;default:''"` + ModelName string `json:"model_name" gorm:"index;default:''"` + Quota int `json:"quota" gorm:"default:0"` + PromptTokens int `json:"prompt_tokens" gorm:"default:0"` + CompletionTokens int `json:"completion_tokens" gorm:"default:0"` } const ( @@ -27,6 +33,7 @@ func RecordLog(userId int, logType int, content string) { } log := &Log{ UserId: userId, + Username: GetUsernameById(userId), CreatedAt: common.GetTimestamp(), Type: logType, Content: content, @@ -37,24 +44,70 @@ func RecordLog(userId int, logType int, content string) { } } -func GetAllLogs(logType int, startIdx int, num int) (logs []*Log, err error) { +func RecordConsumeLog(userId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int, content string) { + if !common.LogConsumeEnabled { + return + } + log := &Log{ + UserId: userId, + Username: GetUsernameById(userId), + CreatedAt: common.GetTimestamp(), + Type: LogTypeConsume, + Content: content, + PromptTokens: promptTokens, + CompletionTokens: completionTokens, + TokenName: tokenName, + ModelName: modelName, + Quota: quota, + } + err := DB.Create(log).Error + if err != nil { + common.SysError("failed to record log: " + err.Error()) + } +} + +func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, startIdx int, num int) (logs []*Log, err error) { var tx *gorm.DB if logType == LogTypeUnknown { tx = DB } else { tx = DB.Where("type = ?", logType) } + if modelName != "" { + tx = tx.Where("model_name = ?", modelName) + } + if username != "" { + tx = tx.Where("username = ?", username) + } + if startTimestamp != 0 { + tx = tx.Where("created_at >= ?", startTimestamp) + } + if endTimestamp != 0 { + tx = tx.Where("created_at <= ?", endTimestamp) + } err = tx.Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error return logs, err } -func GetUserLogs(userId int, logType int, startIdx int, num int) (logs []*Log, err error) { +func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int) (logs []*Log, err error) { var tx *gorm.DB if logType == LogTypeUnknown { tx = DB.Where("user_id = ?", userId) } else { tx = DB.Where("user_id = ? and type = ?", userId, logType) } + if modelName != "" { + tx = tx.Where("model_name = ?", modelName) + } + if tokenName != "" { + tx = tx.Where("token_name = ?", tokenName) + } + if startTimestamp != 0 { + tx = tx.Where("created_at >= ?", startTimestamp) + } + if endTimestamp != 0 { + tx = tx.Where("created_at <= ?", endTimestamp) + } err = tx.Order("id desc").Limit(num).Offset(startIdx).Omit("id").Find(&logs).Error return logs, err } @@ -68,3 +121,45 @@ func SearchUserLogs(userId int, keyword string) (logs []*Log, err error) { err = DB.Where("user_id = ? and type = ?", userId, keyword).Order("id desc").Limit(common.MaxRecentItems).Omit("id").Find(&logs).Error return logs, err } + +func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string) (quota int) { + tx := DB.Table("logs").Select("sum(quota)") + if username != "" { + tx = tx.Where("username = ?", username) + } + if tokenName != "" { + tx = tx.Where("token_name = ?", tokenName) + } + if startTimestamp != 0 { + tx = tx.Where("created_at >= ?", startTimestamp) + } + if endTimestamp != 0 { + tx = tx.Where("created_at <= ?", endTimestamp) + } + if modelName != "" { + tx = tx.Where("model_name = ?", modelName) + } + tx.Where("type = ?", LogTypeConsume).Scan("a) + return quota +} + +func SumUsedToken(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string) (token int) { + tx := DB.Table("logs").Select("sum(prompt_tokens) + sum(completion_tokens)") + if username != "" { + tx = tx.Where("username = ?", username) + } + if tokenName != "" { + tx = tx.Where("token_name = ?", tokenName) + } + if startTimestamp != 0 { + tx = tx.Where("created_at >= ?", startTimestamp) + } + if endTimestamp != 0 { + tx = tx.Where("created_at <= ?", endTimestamp) + } + if modelName != "" { + tx = tx.Where("model_name = ?", modelName) + } + tx.Where("type = ?", LogTypeConsume).Scan(&token) + return token +} diff --git a/model/user.go b/model/user.go index 726991e5..7c771840 100644 --- a/model/user.go +++ b/model/user.go @@ -303,3 +303,8 @@ func UpdateUserUsedQuotaAndRequestCount(id int, quota int) { common.SysError("failed to update user used quota and request count: " + err.Error()) } } + +func GetUsernameById(id int) (username string) { + DB.Model(&User{}).Where("id = ?", id).Select("username").Find(&username) + return username +} diff --git a/router/api-router.go b/router/api-router.go index 2e5cd7d4..3bbac17e 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -96,6 +96,8 @@ func SetApiRouter(router *gin.Engine) { } logRoute := apiRouter.Group("/log") logRoute.GET("/", middleware.AdminAuth(), controller.GetAllLogs) + logRoute.GET("/stat", middleware.AdminAuth(), controller.GetLogsStat) + logRoute.GET("/self/stat", middleware.UserAuth(), controller.GetLogsSelfStat) logRoute.GET("/search", middleware.AdminAuth(), controller.SearchAllLogs) logRoute.GET("/self", middleware.UserAuth(), controller.GetUserLogs) logRoute.GET("/self/search", middleware.UserAuth(), controller.SearchUserLogs) diff --git a/web/src/components/LogsTable.js b/web/src/components/LogsTable.js index e5ec5dfa..a775a240 100644 --- a/web/src/components/LogsTable.js +++ b/web/src/components/LogsTable.js @@ -1,8 +1,9 @@ import React, { useEffect, useState } from 'react'; -import { Button, Label, Pagination, Select, Table } from 'semantic-ui-react'; +import { Button, Form, Header, Label, Pagination, Segment, Select, Table } from 'semantic-ui-react'; import { API, isAdmin, showError, timestamp2string } from '../helpers'; import { ITEMS_PER_PAGE } from '../constants'; +import { renderQuota } from '../helpers/render'; function renderTimestamp(timestamp) { return ( @@ -14,7 +15,7 @@ function renderTimestamp(timestamp) { const MODE_OPTIONS = [ { key: 'all', text: '全部用户', value: 'all' }, - { key: 'self', text: '当前用户', value: 'self' }, + { key: 'self', text: '当前用户', value: 'self' } ]; const LOG_OPTIONS = [ @@ -47,13 +48,57 @@ const LogsTable = () => { const [searchKeyword, setSearchKeyword] = useState(''); const [searching, setSearching] = useState(false); const [logType, setLogType] = useState(0); - const [mode, setMode] = useState('self'); // all, self - const showModePanel = isAdmin(); + const isAdminUser = isAdmin(); + let now = new Date(); + const [inputs, setInputs] = useState({ + name: '', + model_name: '', + start_timestamp: timestamp2string(0), + end_timestamp: timestamp2string(now.getTime() / 1000 + 3600) + }); + const { name, model_name, start_timestamp, end_timestamp } = inputs; + + const [stat, setStat] = useState({ + quota: 0, + token: 0 + }); + + const handleInputChange = (e, { name, value }) => { + setInputs((inputs) => ({ ...inputs, [name]: value })); + }; + + const getLogSelfStat = async () => { + let localStartTimestamp = Date.parse(start_timestamp) / 1000; + let localEndTimestamp = Date.parse(end_timestamp) / 1000; + let res = await API.get(`/api/log/self/stat?type=${logType}&token_name=${name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`); + const { success, message, data } = res.data; + if (success) { + setStat(data); + } else { + showError(message); + } + }; + + const getLogStat = async () => { + let localStartTimestamp = Date.parse(start_timestamp) / 1000; + let localEndTimestamp = Date.parse(end_timestamp) / 1000; + let res = await API.get(`/api/log/stat?type=${logType}&username=${name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`); + const { success, message, data } = res.data; + if (success) { + setStat(data); + } else { + showError(message); + } + }; const loadLogs = async (startIdx) => { - let url = `/api/log/self/?p=${startIdx}&type=${logType}`; - if (mode === 'all') { - url = `/api/log/?p=${startIdx}&type=${logType}`; + let url = ''; + let localStartTimestamp = Date.parse(start_timestamp) / 1000; + let localEndTimestamp = Date.parse(end_timestamp) / 1000; + if (isAdminUser) { + url = `/api/log/?p=${startIdx}&type=${logType}&username=${name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; + } else { + url = `/api/log/self/?p=${startIdx}&type=${logType}&token_name=${name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; } const res = await API.get(url); const { success, message, data } = res.data; @@ -84,19 +129,16 @@ const LogsTable = () => { const refresh = async () => { setLoading(true); await loadLogs(0); + if (isAdminUser) { + getLogStat().then(); + } else { + getLogSelfStat().then(); + } }; - useEffect(() => { - loadLogs(0) - .then() - .catch((reason) => { - showError(reason); - }); - }, []); - useEffect(() => { refresh().then(); - }, [mode, logType]); + }, [logType]); const searchLogs = async () => { if (searchKeyword === '') { @@ -137,118 +179,161 @@ const LogsTable = () => { return ( <> - - - - { - sortLog('created_time'); - }} - width={3} - > - 时间 - - { - showModePanel && ( - { - sortLog('user_id'); - }} - width={1} - > - 用户 - - ) - } - { - sortLog('type'); - }} - width={2} - > - 类型 - - { - sortLog('content'); - }} - width={showModePanel ? 10 : 11} - > - 详情 - - - - - - {logs - .slice( - (activePage - 1) * ITEMS_PER_PAGE, - activePage * ITEMS_PER_PAGE - ) - .map((log, idx) => { - if (log.deleted) return <>; - return ( - - {renderTimestamp(log.created_at)} - { - showModePanel && ( - - ) - } - {renderType(log.type)} - {log.content} - - ); - })} - - - - - - { - showModePanel && ( - { - setLogType(value); + +
使用明细(总消耗额度:{renderQuota(stat.quota)})
+
+ + + + + + 查询 + + +
+ + + { + sortLog('created_time'); }} - /> - - - - - -
+ width={3} + > + 时间 + + + {isAdminUser ? '用户' : '令牌'} + + { + sortLog('type'); + }} + width={2} + > + 类型 + + { + sortLog('model_name'); + }} + width={2} + > + 模型 + + { + sortLog('prompt_tokens'); + }} + width={1} + > + 提示令牌 + + { + sortLog('completion_tokens'); + }} + width={1} + > + 补全令牌 + + { + sortLog('quota'); + }} + width={2} + > + 消耗额度 + + { + sortLog('content'); + }} + width={4} + > + 详情 + + + + + + {logs + .slice( + (activePage - 1) * ITEMS_PER_PAGE, + activePage * ITEMS_PER_PAGE + ) + .map((log, idx) => { + if (log.deleted) return <>; + return ( + + {renderTimestamp(log.created_at)} + { + isAdminUser && ( + {log.username ? : ''} + ) + } + { + !isAdminUser && ( + {log.token_name ? : ''} + ) + } + {renderType(log.type)} + {log.model_name ? : ''} + {log.prompt_tokens ? log.prompt_tokens: ''} + {log.completion_tokens ? log.completion_tokens: ''} + {log.quota ? renderQuota(log.quota, 6) : ''} + {log.content} + + ); + })} + + + + + +