From cab01a8ebb368380d32224945298261b4ba35b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E6=B0=B4=E6=BA=B6?= Date: Sun, 17 Mar 2024 21:42:56 +0900 Subject: [PATCH] feat: save and query request client ip for relay router; fix air theme bugs at log page; --- controller/log.go | 3 +- model/log.go | 11 +++++- relay/controller/audio.go | 12 ++++-- relay/controller/helper.go | 9 +++-- relay/controller/image.go | 9 +++-- relay/controller/text.go | 9 +++-- relay/util/common.go | 13 ++++--- web/air/src/components/LogsTable.js | 37 ++++++++++++------- web/air/src/helpers/utils.js | 4 ++ .../src/views/Log/component/TableHead.js | 1 + web/berry/src/views/Log/component/TableRow.js | 1 + .../src/views/Log/component/TableToolBar.js | 25 +++++++++++++ web/berry/src/views/Log/index.js | 3 +- web/default/src/components/LogsTable.js | 29 ++++++++++++--- 14 files changed, 120 insertions(+), 46 deletions(-) diff --git a/controller/log.go b/controller/log.go index 9377b338..87f681ce 100644 --- a/controller/log.go +++ b/controller/log.go @@ -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, diff --git a/model/log.go b/model/log.go index 4409f73e..c5ef5f64 100644 --- a/model/log.go +++ b/model/log.go @@ -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 int64, content string) { +func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int64, 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: int(quota), ChannelId: channelId, + ClientIP: clientIP, } err := LOG_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 = LOG_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 } diff --git a/relay/controller/audio.go b/relay/controller/audio.go index 155954d2..a65d0d5c 100644 --- a/relay/controller/audio.go +++ b/relay/controller/audio.go @@ -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 { @@ -99,6 +100,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") } @@ -204,7 +208,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 { diff --git a/relay/controller/helper.go b/relay/controller/helper.go index 600a8d65..68c4e5c1 100644 --- a/relay/controller/helper.go +++ b/relay/controller/helper.go @@ -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 int64, modelRatio float64, groupRatio float64) { +func postConsumeQuota(ctx context.Context, usage *relaymodel.Usage, meta *util.RelayMeta, textRequest *relaymodel.GeneralOpenAIRequest, ratio float64, preConsumedQuota int64, 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) } diff --git a/relay/controller/image.go b/relay/controller/image.go index 20ea0a4c..119813da 100644 --- a/relay/controller/image.go +++ b/relay/controller/image.go @@ -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) diff --git a/relay/controller/text.go b/relay/controller/text.go index ba008713..35d0e032 100644 --- a/relay/controller/text.go +++ b/relay/controller/text.go @@ -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 { @@ -99,6 +100,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 } diff --git a/relay/util/common.go b/relay/util/common.go index 535ef680..bf639242 100644 --- a/relay/util/common.go +++ b/relay/util/common.go @@ -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" ) @@ -155,7 +156,7 @@ func GetFullRequestURL(baseURL string, requestURL string, channelType int) strin return fullRequestURL } -func PostConsumeQuota(ctx context.Context, tokenId int, quotaDelta int64, totalQuota int64, userId int, channelId int, modelRatio float64, groupRatio float64, modelName string, tokenName string) { +func PostConsumeQuota(ctx context.Context, tokenId int, quotaDelta int64, totalQuota int64, 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 { @@ -168,7 +169,7 @@ 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, userId, channelId, int(totalQuota), 0, modelName, tokenName, totalQuota, logContent, clientIP) model.UpdateUserUsedQuotaAndRequestCount(userId, totalQuota) model.UpdateChannelUsedQuota(channelId, totalQuota) } diff --git a/web/air/src/components/LogsTable.js b/web/air/src/components/LogsTable.js index 004188c3..e77e5dd8 100644 --- a/web/air/src/components/LogsTable.js +++ b/web/air/src/components/LogsTable.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { API, copy, isAdmin, showError, showSuccess, timestamp2string } from '../helpers'; +import { API, copy, isAdmin, showError, showSuccess, timestamp2string, safeGetLocalTimestamp } from '../helpers'; import { Avatar, Button, Form, Layout, Modal, Select, Space, Spin, Table, Tag } from '@douyinfe/semi-ui'; import { ITEMS_PER_PAGE } from '../constants'; @@ -128,6 +128,10 @@ const LogsTable = () => { {renderQuota(text, 6)} : <>); } + }, { + title: '请求端IP', dataIndex: 'client_ip', render: (text, record, index) => { + return text; + } }, { title: '详情', dataIndex: 'content', render: (text, record, index) => { return { model_name: '', start_timestamp: timestamp2string(now.getTime() / 1000 - 86400), 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 @@ -169,9 +174,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 localStartTimestamp = safeGetLocalTimestamp(start_timestamp); + let localEndTimestamp = safeGetLocalTimestamp(end_timestamp); + 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}&client_ip=${client_ip}`); const { success, message, data } = res.data; if (success) { setStat(data); @@ -181,9 +186,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 localStartTimestamp = safeGetLocalTimestamp(start_timestamp); + let localEndTimestamp = safeGetLocalTimestamp(end_timestamp); + 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}&client_ip=${client_ip}`); const { success, message, data } = res.data; if (success) { setStat(data); @@ -238,12 +243,12 @@ const LogsTable = () => { setLoading(true); let url = ''; - let localStartTimestamp = Date.parse(start_timestamp) / 1000; - let localEndTimestamp = Date.parse(end_timestamp) / 1000; + let localStartTimestamp = safeGetLocalTimestamp(start_timestamp); + let localEndTimestamp = safeGetLocalTimestamp(end_timestamp); if (isAdminUser) { - url = `/api/log/?p=${startIdx}&page_size=${pageSize}&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}&page_size=${pageSize}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&client_ip=${client_ip}`; } else { - url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; + url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&client_ip=${client_ip}`; } const res = await API.get(url); const { success, message, data } = res.data; @@ -349,6 +354,10 @@ const LogsTable = () => { placeholder="可选值" name="model_name" onChange={value => handleInputChange(value, 'model_name')} /> + handleInputChange(value, 'client_ip')} /> { } + onClick={()=>refresh(logType)} loading={loading}>查询 diff --git a/web/air/src/helpers/utils.js b/web/air/src/helpers/utils.js index 580c77ce..c39b254f 100644 --- a/web/air/src/helpers/utils.js +++ b/web/air/src/helpers/utils.js @@ -230,4 +230,8 @@ export function shouldShowPrompt(id) { export function setPromptShown(id) { localStorage.setItem(`prompt-${id}`, 'true'); +} + +export function safeGetLocalTimestamp(timeString) { + return timeString ? Date.parse(timeString) / 1000 : '' } \ No newline at end of file diff --git a/web/berry/src/views/Log/component/TableHead.js b/web/berry/src/views/Log/component/TableHead.js index 671170ce..2170d9f8 100644 --- a/web/berry/src/views/Log/component/TableHead.js +++ b/web/berry/src/views/Log/component/TableHead.js @@ -14,6 +14,7 @@ const LogTableHead = ({ userIsAdmin }) => { 提示 补全 额度 + 请求端IP 详情 diff --git a/web/berry/src/views/Log/component/TableRow.js b/web/berry/src/views/Log/component/TableRow.js index 36b6b666..2fd0c7b2 100644 --- a/web/berry/src/views/Log/component/TableRow.js +++ b/web/berry/src/views/Log/component/TableRow.js @@ -57,6 +57,7 @@ export default function LogTableRow({ item, userIsAdmin }) { {item.prompt_tokens || ''} {item.completion_tokens || ''} {item.quota ? renderQuota(item.quota, 6) : ''} + {item.client_ip || ''} {item.content} diff --git a/web/berry/src/views/Log/component/TableToolBar.js b/web/berry/src/views/Log/component/TableToolBar.js index fec6907b..f74d24b9 100644 --- a/web/berry/src/views/Log/component/TableToolBar.js +++ b/web/berry/src/views/Log/component/TableToolBar.js @@ -5,6 +5,7 @@ import { IconKey, IconBrandGithubCopilot, IconSitemap, + IconMapPin } from "@tabler/icons-react"; import { InputAdornment, @@ -81,6 +82,30 @@ export default function TableToolBar({ /> + + 请求端IP + + + + } + /> + + { 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, @@ -73,7 +74,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 +88,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 +115,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; @@ -211,6 +216,9 @@ const LogsTable = () => { + @@ -321,6 +329,15 @@ const LogsTable = () => { > 额度 + { + sortLog('client_ip'); + }} + width={1} + > + 请求端IP + {