From 1fead8e7f7d99b157718a63a0251fb2608f0a327 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 31 Jan 2025 17:26:21 +0800 Subject: [PATCH 001/106] chore: add debug log for distributor --- middleware/distributor.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/middleware/distributor.go b/middleware/distributor.go index 0aceb29d..58ae0556 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -2,13 +2,15 @@ package middleware import ( "fmt" + "net/http" + "strconv" + "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/model" "github.com/songquanpeng/one-api/relay/channeltype" - "net/http" - "strconv" ) type ModelRequest struct { @@ -17,6 +19,7 @@ type ModelRequest struct { func Distribute() func(c *gin.Context) { return func(c *gin.Context) { + ctx := c.Request.Context() userId := c.GetInt(ctxkey.Id) userGroup, _ := model.CacheGetUserGroup(userId) c.Set(ctxkey.Group, userGroup) @@ -52,6 +55,7 @@ func Distribute() func(c *gin.Context) { return } } + logger.Debugf(ctx, "user id %d, user group: %s, request model: %s, using channel #%d", userId, userGroup, requestModel, channel.Id) SetupContextForSelectedChannel(c, channel, requestModel) c.Next() } From d0402f90863a10036bfc0ec7901295ba14dbf0e5 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 31 Jan 2025 17:54:04 +0800 Subject: [PATCH 002/106] feat: record request_id --- common/helper/helper.go | 19 +++++++++++++++++-- common/logger/logger.go | 10 +++++----- controller/auth/github.go | 11 +++++++---- controller/auth/lark.go | 11 +++++++---- controller/auth/oidc.go | 11 +++++++---- controller/auth/wechat.go | 11 +++++++---- controller/user.go | 15 ++++++++++----- middleware/request-id.go | 4 ++-- model/log.go | 35 +++++++++++++++++++---------------- model/redemption.go | 9 ++++++--- model/user.go | 15 +++++++++------ 11 files changed, 96 insertions(+), 55 deletions(-) diff --git a/common/helper/helper.go b/common/helper/helper.go index df7b0a5f..65f4fd29 100644 --- a/common/helper/helper.go +++ b/common/helper/helper.go @@ -1,9 +1,8 @@ package helper import ( + "context" "fmt" - "github.com/gin-gonic/gin" - "github.com/songquanpeng/one-api/common/random" "html/template" "log" "net" @@ -11,6 +10,10 @@ import ( "runtime" "strconv" "strings" + + "github.com/gin-gonic/gin" + + "github.com/songquanpeng/one-api/common/random" ) func OpenBrowser(url string) { @@ -106,6 +109,18 @@ func GenRequestID() string { return GetTimeString() + random.GetRandomNumberString(8) } +func SetRequestID(ctx context.Context, id string) context.Context { + return context.WithValue(ctx, RequestIdKey, id) +} + +func GetRequestID(ctx context.Context) string { + rawRequestId := ctx.Value(RequestIdKey) + if rawRequestId == nil { + return "" + } + return rawRequestId.(string) +} + func GetResponseID(c *gin.Context) string { logID := c.GetString(RequestIdKey) return fmt.Sprintf("chatcmpl-%s", logID) diff --git a/common/logger/logger.go b/common/logger/logger.go index c5797217..1e3bc254 100644 --- a/common/logger/logger.go +++ b/common/logger/logger.go @@ -113,16 +113,16 @@ func logHelper(ctx context.Context, level loggerLevel, msg string) { if level == loggerINFO { writer = gin.DefaultWriter } - var logId string + var requestId string if ctx != nil { - rawLogId := ctx.Value(helper.RequestIdKey) - if rawLogId != nil { - logId = fmt.Sprintf(" | %s", rawLogId.(string)) + rawRequestId := helper.GetRequestID(ctx) + if rawRequestId != "" { + requestId = fmt.Sprintf(" | %s", rawRequestId) } } lineInfo, funcName := getLineInfo() now := time.Now() - _, _ = fmt.Fprintf(writer, "[%s] %v%s%s %s%s \n", level, now.Format("2006/01/02 - 15:04:05"), logId, lineInfo, funcName, msg) + _, _ = fmt.Fprintf(writer, "[%s] %v%s%s %s%s \n", level, now.Format("2006/01/02 - 15:04:05"), requestId, lineInfo, funcName, msg) SetupLogger() if level == loggerFatal { os.Exit(1) diff --git a/controller/auth/github.go b/controller/auth/github.go index 15542655..ecdd183c 100644 --- a/controller/auth/github.go +++ b/controller/auth/github.go @@ -5,16 +5,18 @@ import ( "encoding/json" "errors" "fmt" + "net/http" + "strconv" + "time" + "github.com/gin-contrib/sessions" "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/random" "github.com/songquanpeng/one-api/controller" "github.com/songquanpeng/one-api/model" - "net/http" - "strconv" - "time" ) type GitHubOAuthResponse struct { @@ -81,6 +83,7 @@ func getGitHubUserInfoByCode(code string) (*GitHubUser, error) { } func GitHubOAuth(c *gin.Context) { + ctx := c.Request.Context() session := sessions.Default(c) state := c.Query("state") if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) { @@ -136,7 +139,7 @@ func GitHubOAuth(c *gin.Context) { user.Role = model.RoleCommonUser user.Status = model.UserStatusEnabled - if err := user.Insert(0); err != nil { + if err := user.Insert(ctx, 0); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": err.Error(), diff --git a/controller/auth/lark.go b/controller/auth/lark.go index 39088b3c..651d5874 100644 --- a/controller/auth/lark.go +++ b/controller/auth/lark.go @@ -5,15 +5,17 @@ import ( "encoding/json" "errors" "fmt" + "net/http" + "strconv" + "time" + "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/controller" "github.com/songquanpeng/one-api/model" - "net/http" - "strconv" - "time" ) type LarkOAuthResponse struct { @@ -79,6 +81,7 @@ func getLarkUserInfoByCode(code string) (*LarkUser, error) { } func LarkOAuth(c *gin.Context) { + ctx := c.Request.Context() session := sessions.Default(c) state := c.Query("state") if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) { @@ -125,7 +128,7 @@ func LarkOAuth(c *gin.Context) { user.Role = model.RoleCommonUser user.Status = model.UserStatusEnabled - if err := user.Insert(0); err != nil { + if err := user.Insert(ctx, 0); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": err.Error(), diff --git a/controller/auth/oidc.go b/controller/auth/oidc.go index 7b4ad4b9..1c4eedbe 100644 --- a/controller/auth/oidc.go +++ b/controller/auth/oidc.go @@ -5,15 +5,17 @@ import ( "encoding/json" "errors" "fmt" + "net/http" + "strconv" + "time" + "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/controller" "github.com/songquanpeng/one-api/model" - "net/http" - "strconv" - "time" ) type OidcResponse struct { @@ -87,6 +89,7 @@ func getOidcUserInfoByCode(code string) (*OidcUser, error) { } func OidcAuth(c *gin.Context) { + ctx := c.Request.Context() session := sessions.Default(c) state := c.Query("state") if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) { @@ -142,7 +145,7 @@ func OidcAuth(c *gin.Context) { } else { user.DisplayName = "OIDC User" } - err := user.Insert(0) + err := user.Insert(ctx, 0) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, diff --git a/controller/auth/wechat.go b/controller/auth/wechat.go index a561aec0..9c30b8f0 100644 --- a/controller/auth/wechat.go +++ b/controller/auth/wechat.go @@ -4,14 +4,16 @@ import ( "encoding/json" "errors" "fmt" + "net/http" + "strconv" + "time" + "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/controller" "github.com/songquanpeng/one-api/model" - "net/http" - "strconv" - "time" ) type wechatLoginResponse struct { @@ -52,6 +54,7 @@ func getWeChatIdByCode(code string) (string, error) { } func WeChatAuth(c *gin.Context) { + ctx := c.Request.Context() if !config.WeChatAuthEnabled { c.JSON(http.StatusOK, gin.H{ "message": "管理员未开启通过微信登录以及注册", @@ -87,7 +90,7 @@ func WeChatAuth(c *gin.Context) { user.Role = model.RoleCommonUser user.Status = model.UserStatusEnabled - if err := user.Insert(0); err != nil { + if err := user.Insert(ctx, 0); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": err.Error(), diff --git a/controller/user.go b/controller/user.go index e79881c2..43e529e5 100644 --- a/controller/user.go +++ b/controller/user.go @@ -109,6 +109,7 @@ func Logout(c *gin.Context) { } func Register(c *gin.Context) { + ctx := c.Request.Context() if !config.RegisterEnabled { c.JSON(http.StatusOK, gin.H{ "message": "管理员关闭了新用户注册", @@ -166,7 +167,7 @@ func Register(c *gin.Context) { if config.EmailVerificationEnabled { cleanUser.Email = user.Email } - if err := cleanUser.Insert(inviterId); err != nil { + if err := cleanUser.Insert(ctx, inviterId); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": err.Error(), @@ -362,6 +363,7 @@ func GetSelf(c *gin.Context) { } func UpdateUser(c *gin.Context) { + ctx := c.Request.Context() var updatedUser model.User err := json.NewDecoder(c.Request.Body).Decode(&updatedUser) if err != nil || updatedUser.Id == 0 { @@ -416,7 +418,7 @@ func UpdateUser(c *gin.Context) { return } if originUser.Quota != updatedUser.Quota { - model.RecordLog(originUser.Id, model.LogTypeManage, fmt.Sprintf("管理员将用户额度从 %s修改为 %s", common.LogQuota(originUser.Quota), common.LogQuota(updatedUser.Quota))) + model.RecordLog(ctx, originUser.Id, model.LogTypeManage, fmt.Sprintf("管理员将用户额度从 %s修改为 %s", common.LogQuota(originUser.Quota), common.LogQuota(updatedUser.Quota))) } c.JSON(http.StatusOK, gin.H{ "success": true, @@ -535,6 +537,7 @@ func DeleteSelf(c *gin.Context) { } func CreateUser(c *gin.Context) { + ctx := c.Request.Context() var user model.User err := json.NewDecoder(c.Request.Body).Decode(&user) if err != nil || user.Username == "" || user.Password == "" { @@ -568,7 +571,7 @@ func CreateUser(c *gin.Context) { Password: user.Password, DisplayName: user.DisplayName, } - if err := cleanUser.Insert(0); err != nil { + if err := cleanUser.Insert(ctx, 0); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": err.Error(), @@ -747,6 +750,7 @@ type topUpRequest struct { } func TopUp(c *gin.Context) { + ctx := c.Request.Context() req := topUpRequest{} err := c.ShouldBindJSON(&req) if err != nil { @@ -757,7 +761,7 @@ func TopUp(c *gin.Context) { return } id := c.GetInt("id") - quota, err := model.Redeem(req.Key, id) + quota, err := model.Redeem(ctx, req.Key, id) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -780,6 +784,7 @@ type adminTopUpRequest struct { } func AdminTopUp(c *gin.Context) { + ctx := c.Request.Context() req := adminTopUpRequest{} err := c.ShouldBindJSON(&req) if err != nil { @@ -800,7 +805,7 @@ func AdminTopUp(c *gin.Context) { if req.Remark == "" { req.Remark = fmt.Sprintf("通过 API 充值 %s", common.LogQuota(int64(req.Quota))) } - model.RecordTopupLog(req.UserId, req.Remark, req.Quota) + model.RecordTopupLog(ctx, req.UserId, req.Remark, req.Quota) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", diff --git a/middleware/request-id.go b/middleware/request-id.go index bef09e32..973a63f8 100644 --- a/middleware/request-id.go +++ b/middleware/request-id.go @@ -1,8 +1,8 @@ package middleware import ( - "context" "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common/helper" ) @@ -10,7 +10,7 @@ func RequestId() func(c *gin.Context) { return func(c *gin.Context) { id := helper.GenRequestID() c.Set(helper.RequestIdKey, id) - ctx := context.WithValue(c.Request.Context(), helper.RequestIdKey, id) + ctx := helper.SetRequestID(c.Request.Context(), id) c.Request = c.Request.WithContext(ctx) c.Header(helper.RequestIdKey, id) c.Next() diff --git a/model/log.go b/model/log.go index 58fdd513..1fd7ee84 100644 --- a/model/log.go +++ b/model/log.go @@ -4,11 +4,12 @@ import ( "context" "fmt" + "gorm.io/gorm" + "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/helper" "github.com/songquanpeng/one-api/common/logger" - "gorm.io/gorm" ) type Log struct { @@ -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"` + RequestId string `json:"request_id"` } const ( @@ -34,7 +36,18 @@ const ( LogTypeSystem ) -func RecordLog(userId int, logType int, content string) { +func recordLogHelper(ctx context.Context, log *Log) { + requestId := helper.GetRequestID(ctx) + log.RequestId = requestId + err := LOG_DB.Create(log).Error + if err != nil { + logger.Error(ctx, "failed to record log: "+err.Error()) + return + } + logger.Infof(ctx, "record log: %+v", log) +} + +func RecordLog(ctx context.Context, userId int, logType int, content string) { if logType == LogTypeConsume && !config.LogConsumeEnabled { return } @@ -45,13 +58,10 @@ func RecordLog(userId int, logType int, content string) { Type: logType, Content: content, } - err := LOG_DB.Create(log).Error - if err != nil { - logger.SysError("failed to record log: " + err.Error()) - } + recordLogHelper(ctx, log) } -func RecordTopupLog(userId int, content string, quota int) { +func RecordTopupLog(ctx context.Context, userId int, content string, quota int) { log := &Log{ UserId: userId, Username: GetUsernameById(userId), @@ -60,14 +70,10 @@ func RecordTopupLog(userId int, content string, quota int) { Content: content, Quota: quota, } - err := LOG_DB.Create(log).Error - if err != nil { - logger.SysError("failed to record log: " + err.Error()) - } + recordLogHelper(ctx, log) } func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int64, content 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 } @@ -84,10 +90,7 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke Quota: int(quota), ChannelId: channelId, } - err := LOG_DB.Create(log).Error - if err != nil { - logger.Error(ctx, "failed to record log: "+err.Error()) - } + recordLogHelper(ctx, log) } func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int) (logs []*Log, err error) { diff --git a/model/redemption.go b/model/redemption.go index 45871a71..957a33be 100644 --- a/model/redemption.go +++ b/model/redemption.go @@ -1,11 +1,14 @@ package model import ( + "context" "errors" "fmt" + + "gorm.io/gorm" + "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/helper" - "gorm.io/gorm" ) const ( @@ -48,7 +51,7 @@ func GetRedemptionById(id int) (*Redemption, error) { return &redemption, err } -func Redeem(key string, userId int) (quota int64, err error) { +func Redeem(ctx context.Context, key string, userId int) (quota int64, err error) { if key == "" { return 0, errors.New("未提供兑换码") } @@ -82,7 +85,7 @@ func Redeem(key string, userId int) (quota int64, err error) { if err != nil { return 0, errors.New("兑换失败," + err.Error()) } - RecordLog(userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %s", common.LogQuota(redemption.Quota))) + RecordLog(ctx, userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %s", common.LogQuota(redemption.Quota))) return redemption.Quota, nil } diff --git a/model/user.go b/model/user.go index a964a0d7..a619c901 100644 --- a/model/user.go +++ b/model/user.go @@ -1,16 +1,19 @@ package model import ( + "context" "errors" "fmt" + "strings" + + "gorm.io/gorm" + "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/blacklist" "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/helper" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/common/random" - "gorm.io/gorm" - "strings" ) const ( @@ -114,7 +117,7 @@ func DeleteUserById(id int) (err error) { return user.Delete() } -func (user *User) Insert(inviterId int) error { +func (user *User) Insert(ctx context.Context, inviterId int) error { var err error if user.Password != "" { user.Password, err = common.Password2Hash(user.Password) @@ -130,16 +133,16 @@ func (user *User) Insert(inviterId int) error { return result.Error } if config.QuotaForNewUser > 0 { - RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", common.LogQuota(config.QuotaForNewUser))) + RecordLog(ctx, user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", common.LogQuota(config.QuotaForNewUser))) } if inviterId != 0 { if config.QuotaForInvitee > 0 { _ = IncreaseUserQuota(user.Id, config.QuotaForInvitee) - RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %s", common.LogQuota(config.QuotaForInvitee))) + RecordLog(ctx, user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %s", common.LogQuota(config.QuotaForInvitee))) } if config.QuotaForInviter > 0 { _ = IncreaseUserQuota(inviterId, config.QuotaForInviter) - RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", common.LogQuota(config.QuotaForInviter))) + RecordLog(ctx, inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", common.LogQuota(config.QuotaForInviter))) } } // create default token From ea0721d525b797b4400bc88995d6d5f7f0684d19 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 31 Jan 2025 18:15:43 +0800 Subject: [PATCH 003/106] feat: update log content format --- relay/billing/billing.go | 3 ++- relay/controller/helper.go | 6 ++++-- relay/controller/image.go | 3 ++- web/default/src/components/LogsTable.js | 7 +++++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/relay/billing/billing.go b/relay/billing/billing.go index a99d37ee..f1bf197a 100644 --- a/relay/billing/billing.go +++ b/relay/billing/billing.go @@ -3,6 +3,7 @@ package billing import ( "context" "fmt" + "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/model" ) @@ -31,7 +32,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) + logContent := fmt.Sprintf("%.2f × %.2f", modelRatio, groupRatio) model.RecordConsumeLog(ctx, userId, channelId, int(totalQuota), 0, modelName, tokenName, totalQuota, logContent) model.UpdateUserUsedQuotaAndRequestCount(userId, totalQuota) model.UpdateChannelUsedQuota(channelId, totalQuota) diff --git a/relay/controller/helper.go b/relay/controller/helper.go index 5f5fc90c..2b83c3d9 100644 --- a/relay/controller/helper.go +++ b/relay/controller/helper.go @@ -4,12 +4,14 @@ import ( "context" "errors" "fmt" - "github.com/songquanpeng/one-api/relay/constant/role" "math" "net/http" "strings" + "github.com/songquanpeng/one-api/relay/constant/role" + "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/logger" @@ -123,7 +125,7 @@ func postConsumeQuota(ctx context.Context, usage *relaymodel.Usage, meta *meta.M if systemPromptReset { extraLog = " (注意系统提示词已被重置)" } - logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f,补全倍率 %.2f%s", modelRatio, groupRatio, completionRatio, 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) model.UpdateUserUsedQuotaAndRequestCount(meta.UserId, quota) model.UpdateChannelUsedQuota(meta.ChannelId, quota) diff --git a/relay/controller/image.go b/relay/controller/image.go index 1b69d97d..468da566 100644 --- a/relay/controller/image.go +++ b/relay/controller/image.go @@ -10,6 +10,7 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/logger" @@ -209,7 +210,7 @@ func RelayImageHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus } if quota != 0 { 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.UpdateUserUsedQuotaAndRequestCount(meta.UserId, quota) channelId := c.GetInt(ctxkey.ChannelId) diff --git a/web/default/src/components/LogsTable.js b/web/default/src/components/LogsTable.js index e266d79a..8b4cda7b 100644 --- a/web/default/src/components/LogsTable.js +++ b/web/default/src/components/LogsTable.js @@ -328,7 +328,7 @@ const LogsTable = () => { }} width={isAdminUser ? 4 : 6} > - 详情 + 详情(模型倍率 × 分组倍率 × 补全倍率) @@ -360,7 +360,10 @@ const LogsTable = () => { {log.prompt_tokens ? log.prompt_tokens : ''} {log.completion_tokens ? log.completion_tokens : ''} {log.quota ? renderQuota(log.quota, 6) : ''} - {log.content} + {log.content}{<> +
+ {log.request_id} + }
); })} From dc470ce82e881996f2c0163eeb42ffa139c9f491 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 31 Jan 2025 19:34:22 +0800 Subject: [PATCH 004/106] 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); }} /> - + Date: Fri, 31 Jan 2025 20:02:51 +0800 Subject: [PATCH 005/106] feat: update log table style --- web/default/src/components/LogsTable.js | 55 +++++++++++++++++-------- web/default/src/helpers/render.js | 53 +++++++++++++++++++----- 2 files changed, 79 insertions(+), 29 deletions(-) diff --git a/web/default/src/components/LogsTable.js b/web/default/src/components/LogsTable.js index 39bafc2e..1f528676 100644 --- a/web/default/src/components/LogsTable.js +++ b/web/default/src/components/LogsTable.js @@ -9,13 +9,34 @@ import { Select, Table, } from 'semantic-ui-react'; -import { API, isAdmin, showError, timestamp2string } from '../helpers'; +import { + API, + copy, + isAdmin, + showError, + showSuccess, + showWarning, + timestamp2string, +} from '../helpers'; import { ITEMS_PER_PAGE } from '../constants'; -import { renderQuota } from '../helpers/render'; +import { renderColorLabel, renderQuota } from '../helpers/render'; -function renderTimestamp(timestamp) { - return <>{timestamp2string(timestamp)}; +function renderTimestamp(timestamp, request_id) { + return ( + { + if (await copy(request_id)) { + showSuccess(`已复制请求 ID:${request_id}`); + } else { + showWarning(`请求 ID 复制失败:${request_id}`); + } + }} + style={{ cursor: 'pointer' }} + > + {timestamp2string(timestamp)} + + ); } const MODE_OPTIONS = [ @@ -103,8 +124,6 @@ function renderDetail(log) { )} -
- {log.request_id} ); } @@ -467,7 +486,9 @@ const LogsTable = () => { if (log.deleted) return <>; return ( - {renderTimestamp(log.created_at)} + + {renderTimestamp(log.created_at, log.request_id)} + {isAdminUser && ( {log.channel ? : ''} @@ -475,23 +496,21 @@ const LogsTable = () => { )} {isAdminUser && ( - {log.username ? : ''} + {log.username ? ( + + ) : ( + '' + )} )} - {log.token_name ? ( - - ) : ( - '' - )} + {log.token_name ? renderColorLabel(log.token_name) : ''} {renderType(log.type)} - {log.model_name ? ( - - ) : ( - '' - )} + {log.model_name ? renderColorLabel(log.model_name) : ''} {log.prompt_tokens ? log.prompt_tokens : ''} diff --git a/web/default/src/helpers/render.js b/web/default/src/helpers/render.js index a9c81cc1..b622f8a6 100644 --- a/web/default/src/helpers/render.js +++ b/web/default/src/helpers/render.js @@ -13,16 +13,18 @@ export function renderGroup(group) { } let groups = group.split(','); groups.sort(); - return <> - {groups.map((group) => { - if (group === 'vip' || group === 'pro') { - return ; - } else if (group === 'svip' || group === 'premium') { - return ; - } - return ; - })} - ; + return ( + <> + {groups.map((group) => { + if (group === 'vip' || group === 'pro') { + return ; + } else if (group === 'svip' || group === 'premium') { + return ; + } + return ; + })} + + ); } export function renderNumber(num) { @@ -55,4 +57,33 @@ export function renderQuotaWithPrompt(quota, digits) { return `(等价金额:${renderQuota(quota, digits)})`; } return ''; -} \ No newline at end of file +} + +const colors = [ + 'red', + 'orange', + 'yellow', + 'olive', + 'green', + 'teal', + 'blue', + 'violet', + 'purple', + 'pink', + 'brown', + 'grey', + 'black', +]; + +export function renderColorLabel(text) { + let hash = 0; + for (let i = 0; i < text.length; i++) { + hash = text.charCodeAt(i) + ((hash << 5) - hash); + } + let index = Math.abs(hash % colors.length); + return ( + + ); +} From 0230d366431fc79328aae5bc787d45b6d3d4a1ca Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 31 Jan 2025 20:06:43 +0800 Subject: [PATCH 006/106] feat: update log table style --- web/default/src/components/LogsTable.js | 4 +--- web/default/src/helpers/render.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/web/default/src/components/LogsTable.js b/web/default/src/components/LogsTable.js index 1f528676..16d2bc92 100644 --- a/web/default/src/components/LogsTable.js +++ b/web/default/src/components/LogsTable.js @@ -497,9 +497,7 @@ const LogsTable = () => { {isAdminUser && ( {log.username ? ( - + ) : ( '' )} diff --git a/web/default/src/helpers/render.js b/web/default/src/helpers/render.js index b622f8a6..24b70464 100644 --- a/web/default/src/helpers/render.js +++ b/web/default/src/helpers/render.js @@ -82,7 +82,7 @@ export function renderColorLabel(text) { } let index = Math.abs(hash % colors.length); return ( - } + trigger={ + + } content='本渠道被程序自动禁用' basic /> @@ -230,15 +252,35 @@ const ChannelsTable = () => { let time = responseTime / 1000; time = time.toFixed(2) + ' 秒'; if (responseTime === 0) { - return ; + return ( + + ); } else if (responseTime <= 1000) { - return ; + return ( + + ); } else if (responseTime <= 3000) { - return ; + return ( + + ); } else if (responseTime <= 5000) { - return ; + return ( + + ); } else { - return ; + return ( + + ); } }; @@ -277,7 +319,11 @@ const ChannelsTable = () => { newChannels[realIdx].response_time = time * 1000; newChannels[realIdx].test_time = Date.now() / 1000; setChannels(newChannels); - showInfo(`渠道 ${name} 测试成功,模型 ${model},耗时 ${time.toFixed(2)} 秒。`); + showInfo( + `渠道 ${name} 测试成功,模型 ${model},耗时 ${time.toFixed( + 2 + )} 秒,模型输出:${message}` + ); } else { showError(message); } @@ -360,7 +406,6 @@ const ChannelsTable = () => { setLoading(false); }; - return ( <>
@@ -374,20 +419,22 @@ const ChannelsTable = () => { onChange={handleKeywordChange} /> - { - showPrompt && ( - { + {showPrompt && ( + { setShowPrompt(false); setPromptShown(promptID); - }}> - OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。 -
- 渠道测试仅支持 chat 模型,优先使用 gpt-3.5-turbo,如果该模型不可用则使用你所配置的模型列表中的第一个模型。 -
- 点击下方详情按钮可以显示余额以及设置额外的测试模型。 -
- ) - } + }} + > + OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为 + 0。对于支持的渠道类型,请点击余额进行刷新。 +
+ 渠道测试仅支持 chat 模型,优先使用 + gpt-3.5-turbo,如果该模型不可用则使用你所配置的模型列表中的第一个模型。 +
+ 点击下方详情按钮可以显示余额以及设置额外的测试模型。 +
+ )}
@@ -478,7 +525,11 @@ const ChannelsTable = () => { {renderStatus(channel.status)} { { - manageChannel( - channel.id, - 'priority', - idx, - event.target.value - ); - }}> - - } + trigger={ + { + manageChannel( + channel.id, + 'priority', + idx, + event.target.value + ); + }} + > + + + } content='渠道选择优先级,越高越优先' basic /> @@ -528,7 +590,12 @@ const ChannelsTable = () => { size={'small'} positive onClick={() => { - testChannel(channel.id, channel.name, idx, channel.test_model); + testChannel( + channel.id, + channel.name, + idx, + channel.test_model + ); }} > 测试 @@ -590,14 +657,31 @@ const ChannelsTable = () => { - - - - {/* @@ -627,8 +716,12 @@ const ChannelsTable = () => { (channels.length % ITEMS_PER_PAGE === 0 ? 1 : 0) } /> - - + + diff --git a/web/default/src/components/LogsTable.js b/web/default/src/components/LogsTable.js index 12b8dc60..1ae9fd6e 100644 --- a/web/default/src/components/LogsTable.js +++ b/web/default/src/components/LogsTable.js @@ -21,6 +21,7 @@ import { import { ITEMS_PER_PAGE } from '../constants'; import { renderColorLabel, renderQuota } from '../helpers/render'; +import { Link } from 'react-router-dom'; function renderTimestamp(timestamp, request_id) { return ( @@ -50,6 +51,7 @@ const LOG_OPTIONS = [ { key: '2', text: '消费', value: 2 }, { key: '3', text: '管理', value: 3 }, { key: '4', text: '系统', value: 4 }, + { key: '5', text: '测试', value: 5 }, ]; function renderType(type) { @@ -78,6 +80,12 @@ function renderType(type) { 系统 ); + case 5: + return ( + + ); default: return (