From dc470ce82e881996f2c0163eeb42ffa139c9f491 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 31 Jan 2025 19:34:22 +0800 Subject: [PATCH] feat: show stream & elapsed time in log detail --- common/helper/time.go | 5 + model/log.go | 47 ++-- relay/billing/billing.go | 11 +- relay/controller/helper.go | 21 +- relay/controller/image.go | 11 +- relay/controller/text.go | 3 +- relay/meta/relay_meta.go | 7 +- web/default/src/components/LogsTable.js | 277 ++++++++++++++++++------ 8 files changed, 277 insertions(+), 105 deletions(-) diff --git a/common/helper/time.go b/common/helper/time.go index 302746db..f0bc6021 100644 --- a/common/helper/time.go +++ b/common/helper/time.go @@ -13,3 +13,8 @@ func GetTimeString() string { now := time.Now() return fmt.Sprintf("%s%d", now.Format("20060102150405"), now.UnixNano()%1e9) } + +// CalcElapsedTime return the elapsed time in milliseconds (ms) +func CalcElapsedTime(start time.Time) int64 { + return time.Now().Sub(start).Milliseconds() +} diff --git a/model/log.go b/model/log.go index 1fd7ee84..17525500 100644 --- a/model/log.go +++ b/model/log.go @@ -13,19 +13,22 @@ import ( ) type Log struct { - Id int `json:"id"` - UserId int `json:"user_id" gorm:"index"` - CreatedAt int64 `json:"created_at" gorm:"bigint;index:idx_created_at_type"` - Type int `json:"type" gorm:"index:idx_created_at_type"` - Content string `json:"content"` - Username string `json:"username" gorm:"index:index_username_model_name,priority:2;default:''"` - TokenName string `json:"token_name" gorm:"index;default:''"` - ModelName string `json:"model_name" gorm:"index;index:index_username_model_name,priority:1;default:''"` - Quota int `json:"quota" gorm:"default:0"` - PromptTokens int `json:"prompt_tokens" gorm:"default:0"` - CompletionTokens int `json:"completion_tokens" gorm:"default:0"` - ChannelId int `json:"channel" gorm:"index"` - RequestId string `json:"request_id"` + Id int `json:"id"` + UserId int `json:"user_id" gorm:"index"` + CreatedAt int64 `json:"created_at" gorm:"bigint;index:idx_created_at_type"` + Type int `json:"type" gorm:"index:idx_created_at_type"` + Content string `json:"content"` + Username string `json:"username" gorm:"index:index_username_model_name,priority:2;default:''"` + TokenName string `json:"token_name" gorm:"index;default:''"` + ModelName string `json:"model_name" gorm:"index;index:index_username_model_name,priority:1;default:''"` + Quota int `json:"quota" gorm:"default:0"` + PromptTokens int `json:"prompt_tokens" gorm:"default:0"` + CompletionTokens int `json:"completion_tokens" gorm:"default:0"` + ChannelId int `json:"channel" gorm:"index"` + RequestId string `json:"request_id" gorm:"default:''"` + ElapsedTime int64 `json:"elapsed_time" gorm:"default:0"` // unit is ms + IsStream bool `json:"is_stream" gorm:"default:false"` + SystemPromptReset bool `json:"system_prompt_reset" gorm:"default:false"` } const ( @@ -73,23 +76,13 @@ func RecordTopupLog(ctx context.Context, userId int, content string, quota int) recordLogHelper(ctx, log) } -func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int64, content string) { +func RecordConsumeLog(ctx context.Context, log *Log) { if !config.LogConsumeEnabled { return } - log := &Log{ - UserId: userId, - Username: GetUsernameById(userId), - CreatedAt: helper.GetTimestamp(), - Type: LogTypeConsume, - Content: content, - PromptTokens: promptTokens, - CompletionTokens: completionTokens, - TokenName: tokenName, - ModelName: modelName, - Quota: int(quota), - ChannelId: channelId, - } + log.Username = GetUsernameById(log.UserId) + log.CreatedAt = helper.GetTimestamp() + log.Type = LogTypeConsume recordLogHelper(ctx, log) } diff --git a/relay/billing/billing.go b/relay/billing/billing.go index f1bf197a..8bd086fe 100644 --- a/relay/billing/billing.go +++ b/relay/billing/billing.go @@ -33,7 +33,16 @@ func PostConsumeQuota(ctx context.Context, tokenId int, quotaDelta int64, totalQ // totalQuota is total quota consumed if totalQuota != 0 { logContent := fmt.Sprintf("%.2f × %.2f", modelRatio, groupRatio) - model.RecordConsumeLog(ctx, userId, channelId, int(totalQuota), 0, modelName, tokenName, totalQuota, logContent) + model.RecordConsumeLog(ctx, &model.Log{ + UserId: userId, + ChannelId: channelId, + PromptTokens: int(totalQuota), + CompletionTokens: 0, + ModelName: modelName, + TokenName: tokenName, + Quota: int(totalQuota), + Content: logContent, + }) model.UpdateUserUsedQuotaAndRequestCount(userId, totalQuota) model.UpdateChannelUsedQuota(channelId, totalQuota) } diff --git a/relay/controller/helper.go b/relay/controller/helper.go index 2b83c3d9..3262c017 100644 --- a/relay/controller/helper.go +++ b/relay/controller/helper.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" + "github.com/songquanpeng/one-api/common/helper" "github.com/songquanpeng/one-api/relay/constant/role" "github.com/gin-gonic/gin" @@ -121,12 +122,20 @@ func postConsumeQuota(ctx context.Context, usage *relaymodel.Usage, meta *meta.M if err != nil { logger.Error(ctx, "error update user quota cache: "+err.Error()) } - var extraLog string - if systemPromptReset { - extraLog = " (注意系统提示词已被重置)" - } - logContent := fmt.Sprintf("%.2f × %.2f × %.2f%s", modelRatio, groupRatio, completionRatio, extraLog) - model.RecordConsumeLog(ctx, meta.UserId, meta.ChannelId, promptTokens, completionTokens, textRequest.Model, meta.TokenName, quota, logContent) + logContent := fmt.Sprintf("%.2f × %.2f × %.2f", modelRatio, groupRatio, completionRatio) + model.RecordConsumeLog(ctx, &model.Log{ + UserId: meta.UserId, + ChannelId: meta.ChannelId, + PromptTokens: promptTokens, + CompletionTokens: completionTokens, + ModelName: textRequest.Model, + TokenName: meta.TokenName, + Quota: int(quota), + Content: logContent, + IsStream: meta.IsStream, + ElapsedTime: helper.CalcElapsedTime(meta.StartTime), + SystemPromptReset: systemPromptReset, + }) model.UpdateUserUsedQuotaAndRequestCount(meta.UserId, quota) model.UpdateChannelUsedQuota(meta.ChannelId, quota) } diff --git a/relay/controller/image.go b/relay/controller/image.go index 468da566..24e49969 100644 --- a/relay/controller/image.go +++ b/relay/controller/image.go @@ -211,7 +211,16 @@ func RelayImageHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus if quota != 0 { tokenName := c.GetString(ctxkey.TokenName) logContent := fmt.Sprintf("%.2f × %.2f", modelRatio, groupRatio) - model.RecordConsumeLog(ctx, meta.UserId, meta.ChannelId, 0, 0, imageRequest.Model, tokenName, quota, logContent) + model.RecordConsumeLog(ctx, &model.Log{ + UserId: meta.UserId, + ChannelId: meta.ChannelId, + PromptTokens: 0, + CompletionTokens: 0, + ModelName: imageRequest.Model, + TokenName: tokenName, + Quota: int(quota), + Content: logContent, + }) model.UpdateUserUsedQuotaAndRequestCount(meta.UserId, quota) channelId := c.GetInt(ctxkey.ChannelId) model.UpdateChannelUsedQuota(channelId, quota) diff --git a/relay/controller/text.go b/relay/controller/text.go index 9a47c58b..6a61884d 100644 --- a/relay/controller/text.go +++ b/relay/controller/text.go @@ -4,11 +4,12 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/songquanpeng/one-api/common/config" "io" "net/http" "github.com/gin-gonic/gin" + + "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/relay" "github.com/songquanpeng/one-api/relay/adaptor" diff --git a/relay/meta/relay_meta.go b/relay/meta/relay_meta.go index bcbe1045..6bf070f3 100644 --- a/relay/meta/relay_meta.go +++ b/relay/meta/relay_meta.go @@ -1,12 +1,15 @@ package meta import ( + "strings" + "time" + "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/model" "github.com/songquanpeng/one-api/relay/channeltype" "github.com/songquanpeng/one-api/relay/relaymode" - "strings" ) type Meta struct { @@ -31,6 +34,7 @@ type Meta struct { RequestURLPath string PromptTokens int // only for DoResponse SystemPrompt string + StartTime time.Time } func GetByContext(c *gin.Context) *Meta { @@ -48,6 +52,7 @@ func GetByContext(c *gin.Context) *Meta { APIKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "), RequestURLPath: c.Request.URL.String(), SystemPrompt: c.GetString(ctxkey.SystemPrompt), + StartTime: time.Now(), } cfg, ok := c.Get(ctxkey.Config) if ok { diff --git a/web/default/src/components/LogsTable.js b/web/default/src/components/LogsTable.js index 8b4cda7b..39bafc2e 100644 --- a/web/default/src/components/LogsTable.js +++ b/web/default/src/components/LogsTable.js @@ -1,21 +1,26 @@ import React, { useEffect, useState } from 'react'; -import { Button, Form, Header, Label, Pagination, Segment, 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 ( - <> - {timestamp2string(timestamp)} - - ); + return <>{timestamp2string(timestamp)}; } const MODE_OPTIONS = [ { key: 'all', text: '全部用户', value: 'all' }, - { key: 'self', text: '当前用户', value: 'self' } + { key: 'self', text: '当前用户', value: 'self' }, ]; const LOG_OPTIONS = [ @@ -23,24 +28,87 @@ const LOG_OPTIONS = [ { key: '1', text: '充值', value: 1 }, { key: '2', text: '消费', value: 2 }, { key: '3', text: '管理', value: 3 }, - { key: '4', text: '系统', value: 4 } + { key: '4', text: '系统', value: 4 }, ]; function renderType(type) { switch (type) { case 1: - return ; + return ( + + ); case 2: - return ; + return ( + + ); case 3: - return ; + return ( + + ); case 4: - return ; + return ( + + ); default: - return ; + return ( + + ); } } +function getColorByElapsedTime(elapsedTime) { + if (elapsedTime === undefined || 0) return 'black'; + if (elapsedTime < 1000) return 'green'; + if (elapsedTime < 3000) return 'olive'; + if (elapsedTime < 5000) return 'yellow'; + if (elapsedTime < 10000) return 'orange'; + return 'red'; +} + +function renderDetail(log) { + return ( + <> + 倍率:{log.content} +
+ {log.elapsed_time && ( + + )} + {log.is_stream && ( + <> + + + )} + {log.system_prompt_reset && ( + <> + + + )} +
+ {log.request_id} + + ); +} + const LogsTable = () => { const [logs, setLogs] = useState([]); const [showStat, setShowStat] = useState(false); @@ -57,13 +125,20 @@ const LogsTable = () => { model_name: '', start_timestamp: timestamp2string(0), end_timestamp: timestamp2string(now.getTime() / 1000 + 3600), - channel: '' + channel: '', }); - const { username, token_name, model_name, start_timestamp, end_timestamp, channel } = inputs; + const { + username, + token_name, + model_name, + start_timestamp, + end_timestamp, + channel, + } = inputs; const [stat, setStat] = useState({ quota: 0, - token: 0 + token: 0, }); const handleInputChange = (e, { name, value }) => { @@ -73,7 +148,9 @@ const LogsTable = () => { 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=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`); + let res = await API.get( + `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}` + ); const { success, message, data } = res.data; if (success) { setStat(data); @@ -85,7 +162,9 @@ const LogsTable = () => { 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=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`); + let res = await API.get( + `/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}` + ); const { success, message, data } = res.data; if (success) { setStat(data); @@ -201,37 +280,82 @@ const LogsTable = () => {
使用明细(总消耗额度: {showStat && renderQuota(stat.quota)} - {!showStat && 点击查看} + {!showStat && ( + + 点击查看 + + )} )
- - - - - 查询 + + + + + + 查询 + - { - isAdminUser && <> + {isAdminUser && ( + <> - - - + + - } + )}
@@ -245,8 +369,8 @@ const LogsTable = () => { > 时间 - { - isAdminUser && { sortLog('channel'); @@ -255,9 +379,9 @@ const LogsTable = () => { > 渠道 - } - { - isAdminUser && { sortLog('username'); @@ -266,7 +390,7 @@ const LogsTable = () => { > 用户 - } + )} { @@ -328,7 +452,7 @@ const LogsTable = () => { }} width={isAdminUser ? 4 : 6} > - 详情(模型倍率 × 分组倍率 × 补全倍率) + 详情 @@ -344,26 +468,41 @@ const LogsTable = () => { return ( {renderTimestamp(log.created_at)} - { - isAdminUser && ( - {log.channel ? : ''} - ) - } - { - isAdminUser && ( - {log.username ? : ''} - ) - } - {log.token_name ? : ''} + {isAdminUser && ( + + {log.channel ? : ''} + + )} + {isAdminUser && ( + + {log.username ? : ''} + + )} + + {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}{<> -
- {log.request_id} - }
+ + {log.model_name ? ( + + ) : ( + '' + )} + + + {log.prompt_tokens ? log.prompt_tokens : ''} + + + {log.completion_tokens ? log.completion_tokens : ''} + + + {log.quota ? renderQuota(log.quota, 6) : ''} + + {renderDetail(log)}
); })} @@ -382,7 +521,9 @@ const LogsTable = () => { setLogType(value); }} /> - +