feat: show stream & elapsed time in log detail

This commit is contained in:
JustSong 2025-01-31 19:34:22 +08:00
parent ea0721d525
commit dc470ce82e
8 changed files with 277 additions and 105 deletions

View File

@ -13,3 +13,8 @@ func GetTimeString() string {
now := time.Now() now := time.Now()
return fmt.Sprintf("%s%d", now.Format("20060102150405"), now.UnixNano()%1e9) 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()
}

View File

@ -25,7 +25,10 @@ type Log struct {
PromptTokens int `json:"prompt_tokens" gorm:"default:0"` PromptTokens int `json:"prompt_tokens" gorm:"default:0"`
CompletionTokens int `json:"completion_tokens" gorm:"default:0"` CompletionTokens int `json:"completion_tokens" gorm:"default:0"`
ChannelId int `json:"channel" gorm:"index"` ChannelId int `json:"channel" gorm:"index"`
RequestId string `json:"request_id"` 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 ( const (
@ -73,23 +76,13 @@ func RecordTopupLog(ctx context.Context, userId int, content string, quota int)
recordLogHelper(ctx, log) 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 { if !config.LogConsumeEnabled {
return return
} }
log := &Log{ log.Username = GetUsernameById(log.UserId)
UserId: userId, log.CreatedAt = helper.GetTimestamp()
Username: GetUsernameById(userId), log.Type = LogTypeConsume
CreatedAt: helper.GetTimestamp(),
Type: LogTypeConsume,
Content: content,
PromptTokens: promptTokens,
CompletionTokens: completionTokens,
TokenName: tokenName,
ModelName: modelName,
Quota: int(quota),
ChannelId: channelId,
}
recordLogHelper(ctx, log) recordLogHelper(ctx, log)
} }

View File

@ -33,7 +33,16 @@ func PostConsumeQuota(ctx context.Context, tokenId int, quotaDelta int64, totalQ
// totalQuota is total quota consumed // totalQuota is total quota consumed
if totalQuota != 0 { if totalQuota != 0 {
logContent := fmt.Sprintf("%.2f × %.2f", modelRatio, groupRatio) 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.UpdateUserUsedQuotaAndRequestCount(userId, totalQuota)
model.UpdateChannelUsedQuota(channelId, totalQuota) model.UpdateChannelUsedQuota(channelId, totalQuota)
} }

View File

@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/songquanpeng/one-api/common/helper"
"github.com/songquanpeng/one-api/relay/constant/role" "github.com/songquanpeng/one-api/relay/constant/role"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -121,12 +122,20 @@ func postConsumeQuota(ctx context.Context, usage *relaymodel.Usage, meta *meta.M
if err != nil { if err != nil {
logger.Error(ctx, "error update user quota cache: "+err.Error()) logger.Error(ctx, "error update user quota cache: "+err.Error())
} }
var extraLog string logContent := fmt.Sprintf("%.2f × %.2f × %.2f", modelRatio, groupRatio, completionRatio)
if systemPromptReset { model.RecordConsumeLog(ctx, &model.Log{
extraLog = " (注意系统提示词已被重置)" UserId: meta.UserId,
} ChannelId: meta.ChannelId,
logContent := fmt.Sprintf("%.2f × %.2f × %.2f%s", modelRatio, groupRatio, completionRatio, extraLog) PromptTokens: promptTokens,
model.RecordConsumeLog(ctx, meta.UserId, meta.ChannelId, promptTokens, completionTokens, textRequest.Model, meta.TokenName, quota, logContent) 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.UpdateUserUsedQuotaAndRequestCount(meta.UserId, quota)
model.UpdateChannelUsedQuota(meta.ChannelId, quota) model.UpdateChannelUsedQuota(meta.ChannelId, quota)
} }

View File

@ -211,7 +211,16 @@ func RelayImageHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus
if quota != 0 { if quota != 0 {
tokenName := c.GetString(ctxkey.TokenName) tokenName := c.GetString(ctxkey.TokenName)
logContent := fmt.Sprintf("%.2f × %.2f", modelRatio, groupRatio) 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) model.UpdateUserUsedQuotaAndRequestCount(meta.UserId, quota)
channelId := c.GetInt(ctxkey.ChannelId) channelId := c.GetInt(ctxkey.ChannelId)
model.UpdateChannelUsedQuota(channelId, quota) model.UpdateChannelUsedQuota(channelId, quota)

View File

@ -4,11 +4,12 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/songquanpeng/one-api/common/config"
"io" "io"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/songquanpeng/one-api/common/config"
"github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/common/logger"
"github.com/songquanpeng/one-api/relay" "github.com/songquanpeng/one-api/relay"
"github.com/songquanpeng/one-api/relay/adaptor" "github.com/songquanpeng/one-api/relay/adaptor"

View File

@ -1,12 +1,15 @@
package meta package meta
import ( import (
"strings"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/ctxkey"
"github.com/songquanpeng/one-api/model" "github.com/songquanpeng/one-api/model"
"github.com/songquanpeng/one-api/relay/channeltype" "github.com/songquanpeng/one-api/relay/channeltype"
"github.com/songquanpeng/one-api/relay/relaymode" "github.com/songquanpeng/one-api/relay/relaymode"
"strings"
) )
type Meta struct { type Meta struct {
@ -31,6 +34,7 @@ type Meta struct {
RequestURLPath string RequestURLPath string
PromptTokens int // only for DoResponse PromptTokens int // only for DoResponse
SystemPrompt string SystemPrompt string
StartTime time.Time
} }
func GetByContext(c *gin.Context) *Meta { 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 "), APIKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
RequestURLPath: c.Request.URL.String(), RequestURLPath: c.Request.URL.String(),
SystemPrompt: c.GetString(ctxkey.SystemPrompt), SystemPrompt: c.GetString(ctxkey.SystemPrompt),
StartTime: time.Now(),
} }
cfg, ok := c.Get(ctxkey.Config) cfg, ok := c.Get(ctxkey.Config)
if ok { if ok {

View File

@ -1,21 +1,26 @@
import React, { useEffect, useState } from 'react'; 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 { API, isAdmin, showError, timestamp2string } from '../helpers';
import { ITEMS_PER_PAGE } from '../constants'; import { ITEMS_PER_PAGE } from '../constants';
import { renderQuota } from '../helpers/render'; import { renderQuota } from '../helpers/render';
function renderTimestamp(timestamp) { function renderTimestamp(timestamp) {
return ( return <>{timestamp2string(timestamp)}</>;
<>
{timestamp2string(timestamp)}
</>
);
} }
const MODE_OPTIONS = [ const MODE_OPTIONS = [
{ key: 'all', text: '全部用户', value: 'all' }, { key: 'all', text: '全部用户', value: 'all' },
{ key: 'self', text: '当前用户', value: 'self' } { key: 'self', text: '当前用户', value: 'self' },
]; ];
const LOG_OPTIONS = [ const LOG_OPTIONS = [
@ -23,24 +28,87 @@ const LOG_OPTIONS = [
{ key: '1', text: '充值', value: 1 }, { key: '1', text: '充值', value: 1 },
{ key: '2', text: '消费', value: 2 }, { key: '2', text: '消费', value: 2 },
{ key: '3', text: '管理', value: 3 }, { key: '3', text: '管理', value: 3 },
{ key: '4', text: '系统', value: 4 } { key: '4', text: '系统', value: 4 },
]; ];
function renderType(type) { function renderType(type) {
switch (type) { switch (type) {
case 1: case 1:
return <Label basic color='green'> 充值 </Label>; return (
<Label basic color='green'>
充值
</Label>
);
case 2: case 2:
return <Label basic color='olive'> 消费 </Label>; return (
<Label basic color='olive'>
消费
</Label>
);
case 3: case 3:
return <Label basic color='orange'> 管理 </Label>; return (
<Label basic color='orange'>
管理
</Label>
);
case 4: case 4:
return <Label basic color='purple'> 系统 </Label>; return (
<Label basic color='purple'>
系统
</Label>
);
default: default:
return <Label basic color='black'> 未知 </Label>; return (
<Label basic color='black'>
未知
</Label>
);
} }
} }
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}
<br />
{log.elapsed_time && (
<Label
basic
size={'mini'}
color={getColorByElapsedTime(log.elapsed_time)}
>
{log.elapsed_time} ms
</Label>
)}
{log.is_stream && (
<>
<Label size={'mini'} color='pink'>
Stream
</Label>
</>
)}
{log.system_prompt_reset && (
<>
<Label basic size={'mini'} color='red'>
System Prompt Reset
</Label>
</>
)}
<br />
<code>{log.request_id}</code>
</>
);
}
const LogsTable = () => { const LogsTable = () => {
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const [showStat, setShowStat] = useState(false); const [showStat, setShowStat] = useState(false);
@ -57,13 +125,20 @@ const LogsTable = () => {
model_name: '', model_name: '',
start_timestamp: timestamp2string(0), start_timestamp: timestamp2string(0),
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600), 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({ const [stat, setStat] = useState({
quota: 0, quota: 0,
token: 0 token: 0,
}); });
const handleInputChange = (e, { name, value }) => { const handleInputChange = (e, { name, value }) => {
@ -73,7 +148,9 @@ const LogsTable = () => {
const getLogSelfStat = async () => { const getLogSelfStat = async () => {
let localStartTimestamp = Date.parse(start_timestamp) / 1000; let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_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; const { success, message, data } = res.data;
if (success) { if (success) {
setStat(data); setStat(data);
@ -85,7 +162,9 @@ const LogsTable = () => {
const getLogStat = async () => { const getLogStat = async () => {
let localStartTimestamp = Date.parse(start_timestamp) / 1000; let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_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; const { success, message, data } = res.data;
if (success) { if (success) {
setStat(data); setStat(data);
@ -201,37 +280,82 @@ const LogsTable = () => {
<Header as='h3'> <Header as='h3'>
使用明细总消耗额度 使用明细总消耗额度
{showStat && renderQuota(stat.quota)} {showStat && renderQuota(stat.quota)}
{!showStat && <span onClick={handleEyeClick} style={{ cursor: 'pointer', color: 'gray' }}>点击查看</span>} {!showStat && (
<span
onClick={handleEyeClick}
style={{ cursor: 'pointer', color: 'gray' }}
>
点击查看
</span>
)}
</Header> </Header>
<Form> <Form>
<Form.Group> <Form.Group>
<Form.Input fluid label={'令牌名称'} width={3} value={token_name} <Form.Input
placeholder={'可选值'} name='token_name' onChange={handleInputChange} /> fluid
<Form.Input fluid label='模型名称' width={3} value={model_name} placeholder='可选值' label={'令牌名称'}
width={3}
value={token_name}
placeholder={'可选值'}
name='token_name'
onChange={handleInputChange}
/>
<Form.Input
fluid
label='模型名称'
width={3}
value={model_name}
placeholder='可选值'
name='model_name' name='model_name'
onChange={handleInputChange} /> onChange={handleInputChange}
<Form.Input fluid label='起始时间' width={4} value={start_timestamp} type='datetime-local' />
<Form.Input
fluid
label='起始时间'
width={4}
value={start_timestamp}
type='datetime-local'
name='start_timestamp' name='start_timestamp'
onChange={handleInputChange} /> onChange={handleInputChange}
<Form.Input fluid label='结束时间' width={4} value={end_timestamp} type='datetime-local' />
<Form.Input
fluid
label='结束时间'
width={4}
value={end_timestamp}
type='datetime-local'
name='end_timestamp' name='end_timestamp'
onChange={handleInputChange} /> onChange={handleInputChange}
<Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button> />
<Form.Button fluid label='操作' width={2} onClick={refresh}>
查询
</Form.Button>
</Form.Group> </Form.Group>
{ {isAdminUser && (
isAdminUser && <> <>
<Form.Group> <Form.Group>
<Form.Input fluid label={'渠道 ID'} width={3} value={channel} <Form.Input
placeholder='可选值' name='channel' fluid
onChange={handleInputChange} /> label={'渠道 ID'}
<Form.Input fluid label={'用户名称'} width={3} value={username} width={3}
placeholder={'可选值'} name='username' value={channel}
onChange={handleInputChange} /> placeholder='可选值'
name='channel'
onChange={handleInputChange}
/>
<Form.Input
fluid
label={'用户名称'}
width={3}
value={username}
placeholder={'可选值'}
name='username'
onChange={handleInputChange}
/>
</Form.Group> </Form.Group>
</> </>
} )}
</Form> </Form>
<Table basic compact size='small'> <Table basic compact size='small'>
<Table.Header> <Table.Header>
@ -245,8 +369,8 @@ const LogsTable = () => {
> >
时间 时间
</Table.HeaderCell> </Table.HeaderCell>
{ {isAdminUser && (
isAdminUser && <Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
sortLog('channel'); sortLog('channel');
@ -255,9 +379,9 @@ const LogsTable = () => {
> >
渠道 渠道
</Table.HeaderCell> </Table.HeaderCell>
} )}
{ {isAdminUser && (
isAdminUser && <Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
sortLog('username'); sortLog('username');
@ -266,7 +390,7 @@ const LogsTable = () => {
> >
用户 用户
</Table.HeaderCell> </Table.HeaderCell>
} )}
<Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
@ -328,7 +452,7 @@ const LogsTable = () => {
}} }}
width={isAdminUser ? 4 : 6} width={isAdminUser ? 4 : 6}
> >
详情模型倍率 × 分组倍率 × 补全倍率 详情
</Table.HeaderCell> </Table.HeaderCell>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
@ -344,26 +468,41 @@ const LogsTable = () => {
return ( return (
<Table.Row key={log.id}> <Table.Row key={log.id}>
<Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell> <Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell>
{ {isAdminUser && (
isAdminUser && ( <Table.Cell>
<Table.Cell>{log.channel ? <Label basic>{log.channel}</Label> : ''}</Table.Cell> {log.channel ? <Label basic>{log.channel}</Label> : ''}
) </Table.Cell>
} )}
{ {isAdminUser && (
isAdminUser && ( <Table.Cell>
<Table.Cell>{log.username ? <Label>{log.username}</Label> : ''}</Table.Cell> {log.username ? <Label>{log.username}</Label> : ''}
) </Table.Cell>
} )}
<Table.Cell>{log.token_name ? <Label basic>{log.token_name}</Label> : ''}</Table.Cell> <Table.Cell>
{log.token_name ? (
<Label basic>{log.token_name}</Label>
) : (
''
)}
</Table.Cell>
<Table.Cell>{renderType(log.type)}</Table.Cell> <Table.Cell>{renderType(log.type)}</Table.Cell>
<Table.Cell>{log.model_name ? <Label basic>{log.model_name}</Label> : ''}</Table.Cell> <Table.Cell>
<Table.Cell>{log.prompt_tokens ? log.prompt_tokens : ''}</Table.Cell> {log.model_name ? (
<Table.Cell>{log.completion_tokens ? log.completion_tokens : ''}</Table.Cell> <Label basic>{log.model_name}</Label>
<Table.Cell>{log.quota ? renderQuota(log.quota, 6) : ''}</Table.Cell> ) : (
<Table.Cell>{log.content}{<> ''
<br/> )}
<code>{log.request_id}</code> </Table.Cell>
</>}</Table.Cell> <Table.Cell>
{log.prompt_tokens ? log.prompt_tokens : ''}
</Table.Cell>
<Table.Cell>
{log.completion_tokens ? log.completion_tokens : ''}
</Table.Cell>
<Table.Cell>
{log.quota ? renderQuota(log.quota, 6) : ''}
</Table.Cell>
<Table.Cell>{renderDetail(log)}</Table.Cell>
</Table.Row> </Table.Row>
); );
})} })}
@ -382,7 +521,9 @@ const LogsTable = () => {
setLogType(value); setLogType(value);
}} }}
/> />
<Button size='small' onClick={refresh} loading={loading}>刷新</Button> <Button size='small' onClick={refresh} loading={loading}>
刷新
</Button>
<Pagination <Pagination
floated='right' floated='right'
activePage={activePage} activePage={activePage}