feat: save request client ip & search in form;

This commit is contained in:
林水溶 2024-03-12 00:47:24 +09:00
parent a90161cf00
commit d632d10405
8 changed files with 235 additions and 90 deletions

View File

@ -20,7 +20,8 @@ func GetAllLogs(c *gin.Context) {
tokenName := c.Query("token_name")
modelName := c.Query("model_name")
channel, _ := strconv.Atoi(c.Query("channel"))
logs, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, p*config.ItemsPerPage, config.ItemsPerPage, channel)
clientIP := c.Query("client_ip")
logs, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, p*config.ItemsPerPage, config.ItemsPerPage, channel, clientIP)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,

View File

@ -3,6 +3,7 @@ package model
import (
"context"
"fmt"
"github.com/songquanpeng/one-api/common"
"github.com/songquanpeng/one-api/common/config"
"github.com/songquanpeng/one-api/common/helper"
@ -24,6 +25,7 @@ type Log struct {
PromptTokens int `json:"prompt_tokens" gorm:"default:0"`
CompletionTokens int `json:"completion_tokens" gorm:"default:0"`
ChannelId int `json:"channel" gorm:"index"`
ClientIP string `json:"client_ip" gorm:"index;default:''"`
}
const (
@ -51,7 +53,7 @@ func RecordLog(userId int, logType int, content string) {
}
}
func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int, content string) {
func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int, content string, clientIP string) {
logger.Info(ctx, fmt.Sprintf("record consume log: userId=%d, channelId=%d, promptTokens=%d, completionTokens=%d, modelName=%s, tokenName=%s, quota=%d, content=%s", userId, channelId, promptTokens, completionTokens, modelName, tokenName, quota, content))
if !config.LogConsumeEnabled {
return
@ -68,6 +70,7 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke
ModelName: modelName,
Quota: quota,
ChannelId: channelId,
ClientIP: clientIP,
}
err := DB.Create(log).Error
if err != nil {
@ -75,7 +78,7 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke
}
}
func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int) (logs []*Log, err error) {
func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int, clientIP string) (logs []*Log, err error) {
var tx *gorm.DB
if logType == LogTypeUnknown {
tx = DB
@ -100,6 +103,10 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName
if channel != 0 {
tx = tx.Where("channel_id = ?", channel)
}
if clientIP != "" {
tx = tx.Where("client_ip = ?", clientIP)
}
err = tx.Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error
return logs, err
}

View File

@ -7,6 +7,10 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/songquanpeng/one-api/common"
"github.com/songquanpeng/one-api/common/config"
@ -16,9 +20,6 @@ import (
"github.com/songquanpeng/one-api/relay/constant"
relaymodel "github.com/songquanpeng/one-api/relay/model"
"github.com/songquanpeng/one-api/relay/util"
"io"
"net/http"
"strings"
)
func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatusCode {
@ -98,6 +99,9 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus
baseURL := common.ChannelBaseURLs[channelType]
requestURL := c.Request.URL.String()
clientIP := c.ClientIP()
fmt.Println(clientIP)
if c.GetString("base_url") != "" {
baseURL = c.GetString("base_url")
}
@ -203,7 +207,7 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus
}
quotaDelta := quota - preConsumedQuota
defer func(ctx context.Context) {
go util.PostConsumeQuota(ctx, tokenId, quotaDelta, quota, userId, channelId, modelRatio, groupRatio, audioModel, tokenName)
go util.PostConsumeQuota(ctx, tokenId, quotaDelta, quota, userId, channelId, modelRatio, groupRatio, audioModel, tokenName, clientIP)
}(c.Request.Context())
for k, v := range resp.Header {

View File

@ -4,6 +4,9 @@ import (
"context"
"errors"
"fmt"
"math"
"net/http"
"github.com/gin-gonic/gin"
"github.com/songquanpeng/one-api/common"
"github.com/songquanpeng/one-api/common/config"
@ -13,8 +16,6 @@ import (
"github.com/songquanpeng/one-api/relay/constant"
relaymodel "github.com/songquanpeng/one-api/relay/model"
"github.com/songquanpeng/one-api/relay/util"
"math"
"net/http"
)
func getAndValidateTextRequest(c *gin.Context, relayMode int) (*relaymodel.GeneralOpenAIRequest, error) {
@ -144,7 +145,7 @@ func preConsumeQuota(ctx context.Context, textRequest *relaymodel.GeneralOpenAIR
return preConsumedQuota, nil
}
func postConsumeQuota(ctx context.Context, usage *relaymodel.Usage, meta *util.RelayMeta, textRequest *relaymodel.GeneralOpenAIRequest, ratio float64, preConsumedQuota int, modelRatio float64, groupRatio float64) {
func postConsumeQuota(ctx context.Context, usage *relaymodel.Usage, meta *util.RelayMeta, textRequest *relaymodel.GeneralOpenAIRequest, ratio float64, preConsumedQuota int, modelRatio float64, groupRatio float64, clientIP string) {
if usage == nil {
logger.Error(ctx, "usage is nil, which is unexpected")
return
@ -173,7 +174,7 @@ func postConsumeQuota(ctx context.Context, usage *relaymodel.Usage, meta *util.R
logger.Error(ctx, "error update user quota cache: "+err.Error())
}
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f,补全倍率 %.2f", modelRatio, groupRatio, completionRatio)
model.RecordConsumeLog(ctx, meta.UserId, meta.ChannelId, promptTokens, completionTokens, textRequest.Model, meta.TokenName, quota, logContent)
model.RecordConsumeLog(ctx, meta.UserId, meta.ChannelId, promptTokens, completionTokens, textRequest.Model, meta.TokenName, quota, logContent, clientIP)
model.UpdateUserUsedQuotaAndRequestCount(meta.UserId, quota)
model.UpdateChannelUsedQuota(meta.ChannelId, quota)
}

View File

@ -6,6 +6,10 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/songquanpeng/one-api/common"
"github.com/songquanpeng/one-api/common/logger"
"github.com/songquanpeng/one-api/model"
@ -13,9 +17,6 @@ import (
"github.com/songquanpeng/one-api/relay/constant"
relaymodel "github.com/songquanpeng/one-api/relay/model"
"github.com/songquanpeng/one-api/relay/util"
"io"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
@ -132,7 +133,7 @@ func RelayImageHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus
if quota != 0 {
tokenName := c.GetString("token_name")
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
model.RecordConsumeLog(ctx, meta.UserId, meta.ChannelId, 0, 0, imageRequest.Model, tokenName, quota, logContent)
model.RecordConsumeLog(ctx, meta.UserId, meta.ChannelId, 0, 0, imageRequest.Model, tokenName, quota, logContent, c.ClientIP())
model.UpdateUserUsedQuotaAndRequestCount(meta.UserId, quota)
channelId := c.GetInt("channel_id")
model.UpdateChannelUsedQuota(channelId, quota)

View File

@ -4,6 +4,10 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/songquanpeng/one-api/common"
"github.com/songquanpeng/one-api/common/logger"
@ -12,9 +16,6 @@ import (
"github.com/songquanpeng/one-api/relay/helper"
"github.com/songquanpeng/one-api/relay/model"
"github.com/songquanpeng/one-api/relay/util"
"io"
"net/http"
"strings"
)
func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode {
@ -98,6 +99,6 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode {
return respErr
}
// post-consume quota
go postConsumeQuota(ctx, usage, meta, textRequest, ratio, preConsumedQuota, modelRatio, groupRatio)
go postConsumeQuota(ctx, usage, meta, textRequest, ratio, preConsumedQuota, modelRatio, groupRatio, c.ClientIP())
return nil
}

View File

@ -4,15 +4,16 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/songquanpeng/one-api/common"
"github.com/songquanpeng/one-api/common/config"
"github.com/songquanpeng/one-api/common/logger"
"github.com/songquanpeng/one-api/model"
relaymodel "github.com/songquanpeng/one-api/relay/model"
"io"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
@ -148,7 +149,7 @@ func GetFullRequestURL(baseURL string, requestURL string, channelType int) strin
return fullRequestURL
}
func PostConsumeQuota(ctx context.Context, tokenId int, quotaDelta int, totalQuota int, userId int, channelId int, modelRatio float64, groupRatio float64, modelName string, tokenName string) {
func PostConsumeQuota(ctx context.Context, tokenId int, quotaDelta int, totalQuota int, userId int, channelId int, modelRatio float64, groupRatio float64, modelName string, tokenName string, clientIP string) {
// quotaDelta is remaining quota to be consumed
err := model.PostConsumeTokenQuota(tokenId, quotaDelta)
if err != nil {
@ -161,7 +162,7 @@ func PostConsumeQuota(ctx context.Context, tokenId int, quotaDelta int, totalQuo
// totalQuota is total quota consumed
if totalQuota != 0 {
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
model.RecordConsumeLog(ctx, userId, channelId, totalQuota, 0, modelName, tokenName, totalQuota, logContent)
model.RecordConsumeLog(ctx, userId, channelId, totalQuota, 0, modelName, tokenName, totalQuota, logContent, clientIP)
model.UpdateUserUsedQuotaAndRequestCount(userId, totalQuota)
model.UpdateChannelUsedQuota(channelId, totalQuota)
}

View File

@ -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,21 +28,46 @@ 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 <Label basic color='green'> 充值 </Label>;
return (
<Label basic color='green'>
{' '}
充值{' '}
</Label>
);
case 2:
return <Label basic color='olive'> 消费 </Label>;
return (
<Label basic color='olive'>
{' '}
消费{' '}
</Label>
);
case 3:
return <Label basic color='orange'> 管理 </Label>;
return (
<Label basic color='orange'>
{' '}
管理{' '}
</Label>
);
case 4:
return <Label basic color='purple'> 系统 </Label>;
return (
<Label basic color='purple'>
{' '}
系统{' '}
</Label>
);
default:
return <Label basic color='black'> 未知 </Label>;
return (
<Label basic color='black'>
{' '}
未知{' '}
</Label>
);
}
}
@ -57,13 +87,22 @@ const LogsTable = () => {
model_name: '',
start_timestamp: timestamp2string(0),
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
channel: ''
channel: '',
client_ip: '',
});
const { username, token_name, model_name, start_timestamp, end_timestamp, channel } = inputs;
const {
username,
token_name,
model_name,
start_timestamp,
end_timestamp,
channel,
client_ip,
} = inputs;
const [stat, setStat] = useState({
quota: 0,
token: 0
token: 0,
});
const handleInputChange = (e, { name, value }) => {
@ -73,7 +112,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}&client_ip=${client_ip}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`
);
const { success, message, data } = res.data;
if (success) {
setStat(data);
@ -85,7 +126,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}&client_ip=${client_ip}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`
);
const { success, message, data } = res.data;
if (success) {
setStat(data);
@ -110,9 +153,9 @@ const LogsTable = () => {
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) {
url = `/api/log/?p=${startIdx}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`;
url = `/api/log/?p=${startIdx}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&client_ip=${client_ip}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`;
} else {
url = `/api/log/self/?p=${startIdx}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
url = `/api/log/self/?p=${startIdx}&type=${logType}&token_name=${token_name}&model_name=${model_name}&client_ip=${client_ip}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
}
const res = await API.get(url);
const { success, message, data } = res.data;
@ -201,37 +244,91 @@ const LogsTable = () => {
<Header as='h3'>
使用明细总消耗额度
{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>
<Form>
<Form.Group>
<Form.Input fluid 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'
onChange={handleInputChange} />
<Form.Input fluid label='起始时间' width={4} value={start_timestamp} type='datetime-local'
name='start_timestamp'
onChange={handleInputChange} />
<Form.Input fluid label='结束时间' width={4} value={end_timestamp} type='datetime-local'
name='end_timestamp'
onChange={handleInputChange} />
<Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button>
<Form.Input
fluid
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'
onChange={handleInputChange}
/>
<Form.Input
fluid
label='请求端IP'
width={3}
value={client_ip}
placeholder='可选值'
name='client_ip'
onChange={handleInputChange}
/>
<Form.Input
fluid
label='起始时间'
width={4}
value={start_timestamp}
type='datetime-local'
name='start_timestamp'
onChange={handleInputChange}
/>
<Form.Input
fluid
label='结束时间'
width={4}
value={end_timestamp}
type='datetime-local'
name='end_timestamp'
onChange={handleInputChange}
/>
<Form.Button fluid label='操作' width={2} onClick={refresh}>
查询
</Form.Button>
</Form.Group>
{
isAdminUser && <>
{isAdminUser && (
<>
<Form.Group>
<Form.Input fluid label={'渠道 ID'} width={3} value={channel}
placeholder='可选值' name='channel'
onChange={handleInputChange} />
<Form.Input fluid label={'用户名称'} width={3} value={username}
placeholder={'可选值'} name='username'
onChange={handleInputChange} />
<Form.Input
fluid
label={'渠道 ID'}
width={3}
value={channel}
placeholder='可选值'
name='channel'
onChange={handleInputChange}
/>
<Form.Input
fluid
label={'用户名称'}
width={3}
value={username}
placeholder={'可选值'}
name='username'
onChange={handleInputChange}
/>
</Form.Group>
</>
}
)}
</Form>
<Table basic compact size='small'>
<Table.Header>
@ -245,8 +342,8 @@ const LogsTable = () => {
>
时间
</Table.HeaderCell>
{
isAdminUser && <Table.HeaderCell
{isAdminUser && (
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('channel');
@ -255,9 +352,9 @@ const LogsTable = () => {
>
渠道
</Table.HeaderCell>
}
{
isAdminUser && <Table.HeaderCell
)}
{isAdminUser && (
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('username');
@ -266,7 +363,7 @@ const LogsTable = () => {
>
用户
</Table.HeaderCell>
}
)}
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
@ -321,6 +418,15 @@ const LogsTable = () => {
>
额度
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('client_ip');
}}
width={1}
>
请求IP
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
@ -344,22 +450,43 @@ const LogsTable = () => {
return (
<Table.Row key={log.id}>
<Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell>
{
isAdminUser && (
<Table.Cell>{log.channel ? <Label basic>{log.channel}</Label> : ''}</Table.Cell>
)
}
{
isAdminUser && (
<Table.Cell>{log.username ? <Label>{log.username}</Label> : ''}</Table.Cell>
)
}
<Table.Cell>{log.token_name ? <Label basic>{log.token_name}</Label> : ''}</Table.Cell>
{isAdminUser && (
<Table.Cell>
{log.channel ? <Label basic>{log.channel}</Label> : ''}
</Table.Cell>
)}
{isAdminUser && (
<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>{renderType(log.type)}</Table.Cell>
<Table.Cell>{log.model_name ? <Label basic>{log.model_name}</Label> : ''}</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>
{log.model_name ? (
<Label basic>{log.model_name}</Label>
) : (
''
)}
</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>
{log.client_ip ? log.client_ip : ''}
</Table.Cell>
<Table.Cell>{log.content}</Table.Cell>
</Table.Row>
);
@ -379,7 +506,9 @@ const LogsTable = () => {
setLogType(value);
}}
/>
<Button size='small' onClick={refresh} loading={loading}>刷新</Button>
<Button size='small' onClick={refresh} loading={loading}>
刷新
</Button>
<Pagination
floated='right'
activePage={activePage}