|
|
package model |
|
|
|
|
|
import ( |
|
|
"context" |
|
|
"fmt" |
|
|
"os" |
|
|
"strings" |
|
|
"time" |
|
|
|
|
|
"github.com/QuantumNous/new-api/common" |
|
|
"github.com/QuantumNous/new-api/logger" |
|
|
"github.com/QuantumNous/new-api/types" |
|
|
|
|
|
"github.com/gin-gonic/gin" |
|
|
|
|
|
"github.com/bytedance/gopkg/util/gopool" |
|
|
"gorm.io/gorm" |
|
|
) |
|
|
|
|
|
type Log struct { |
|
|
Id int `json:"id" gorm:"index:idx_created_at_id,priority:1"` |
|
|
UserId int `json:"user_id" gorm:"index"` |
|
|
CreatedAt int64 `json:"created_at" gorm:"bigint;index:idx_created_at_id,priority:2;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: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"` |
|
|
UseTime int `json:"use_time" gorm:"default:0"` |
|
|
IsStream bool `json:"is_stream"` |
|
|
ChannelId int `json:"channel" gorm:"index"` |
|
|
ChannelName string `json:"channel_name" gorm:"->"` |
|
|
TokenId int `json:"token_id" gorm:"default:0;index"` |
|
|
Group string `json:"group" gorm:"index"` |
|
|
Ip string `json:"ip" gorm:"index;default:''"` |
|
|
Other string `json:"other"` |
|
|
} |
|
|
|
|
|
|
|
|
const ( |
|
|
LogTypeUnknown = 0 |
|
|
LogTypeTopup = 1 |
|
|
LogTypeConsume = 2 |
|
|
LogTypeManage = 3 |
|
|
LogTypeSystem = 4 |
|
|
LogTypeError = 5 |
|
|
LogTypeRefund = 6 |
|
|
) |
|
|
|
|
|
func formatUserLogs(logs []*Log) { |
|
|
for i := range logs { |
|
|
logs[i].ChannelName = "" |
|
|
var otherMap map[string]interface{} |
|
|
otherMap, _ = common.StrToMap(logs[i].Other) |
|
|
if otherMap != nil { |
|
|
|
|
|
delete(otherMap, "admin_info") |
|
|
} |
|
|
logs[i].Other = common.MapToJsonStr(otherMap) |
|
|
logs[i].Id = logs[i].Id % 1024 |
|
|
} |
|
|
} |
|
|
|
|
|
func GetLogByKey(key string) (logs []*Log, err error) { |
|
|
if os.Getenv("LOG_SQL_DSN") != "" { |
|
|
var tk Token |
|
|
if err = DB.Model(&Token{}).Where(logKeyCol+"=?", strings.TrimPrefix(key, "sk-")).First(&tk).Error; err != nil { |
|
|
return nil, err |
|
|
} |
|
|
err = LOG_DB.Model(&Log{}).Where("token_id=?", tk.Id).Find(&logs).Error |
|
|
} else { |
|
|
err = LOG_DB.Joins("left join tokens on tokens.id = logs.token_id").Where("tokens.key = ?", strings.TrimPrefix(key, "sk-")).Find(&logs).Error |
|
|
} |
|
|
formatUserLogs(logs) |
|
|
return logs, err |
|
|
} |
|
|
|
|
|
func RecordLog(userId int, logType int, content string) { |
|
|
if logType == LogTypeConsume && !common.LogConsumeEnabled { |
|
|
return |
|
|
} |
|
|
username, _ := GetUsernameById(userId, false) |
|
|
log := &Log{ |
|
|
UserId: userId, |
|
|
Username: username, |
|
|
CreatedAt: common.GetTimestamp(), |
|
|
Type: logType, |
|
|
Content: content, |
|
|
} |
|
|
err := LOG_DB.Create(log).Error |
|
|
if err != nil { |
|
|
common.SysLog("failed to record log: " + err.Error()) |
|
|
} |
|
|
} |
|
|
|
|
|
func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string, tokenName string, content string, tokenId int, useTimeSeconds int, |
|
|
isStream bool, group string, other map[string]interface{}) { |
|
|
logger.LogInfo(c, fmt.Sprintf("record error log: userId=%d, channelId=%d, modelName=%s, tokenName=%s, content=%s", userId, channelId, modelName, tokenName, content)) |
|
|
username := c.GetString("username") |
|
|
otherStr := common.MapToJsonStr(other) |
|
|
|
|
|
needRecordIp := false |
|
|
if settingMap, err := GetUserSetting(userId, false); err == nil { |
|
|
if settingMap.RecordIpLog { |
|
|
needRecordIp = true |
|
|
} |
|
|
} |
|
|
log := &Log{ |
|
|
UserId: userId, |
|
|
Username: username, |
|
|
CreatedAt: common.GetTimestamp(), |
|
|
Type: LogTypeError, |
|
|
Content: content, |
|
|
PromptTokens: 0, |
|
|
CompletionTokens: 0, |
|
|
TokenName: tokenName, |
|
|
ModelName: modelName, |
|
|
Quota: 0, |
|
|
ChannelId: channelId, |
|
|
TokenId: tokenId, |
|
|
UseTime: useTimeSeconds, |
|
|
IsStream: isStream, |
|
|
Group: group, |
|
|
Ip: func() string { |
|
|
if needRecordIp { |
|
|
return c.ClientIP() |
|
|
} |
|
|
return "" |
|
|
}(), |
|
|
Other: otherStr, |
|
|
} |
|
|
err := LOG_DB.Create(log).Error |
|
|
if err != nil { |
|
|
logger.LogError(c, "failed to record log: "+err.Error()) |
|
|
} |
|
|
} |
|
|
|
|
|
type RecordConsumeLogParams struct { |
|
|
ChannelId int `json:"channel_id"` |
|
|
PromptTokens int `json:"prompt_tokens"` |
|
|
CompletionTokens int `json:"completion_tokens"` |
|
|
ModelName string `json:"model_name"` |
|
|
TokenName string `json:"token_name"` |
|
|
Quota int `json:"quota"` |
|
|
Content string `json:"content"` |
|
|
TokenId int `json:"token_id"` |
|
|
UseTimeSeconds int `json:"use_time_seconds"` |
|
|
IsStream bool `json:"is_stream"` |
|
|
Group string `json:"group"` |
|
|
Other map[string]interface{} `json:"other"` |
|
|
} |
|
|
|
|
|
func RecordConsumeLog(c *gin.Context, userId int, params RecordConsumeLogParams) { |
|
|
if !common.LogConsumeEnabled { |
|
|
return |
|
|
} |
|
|
logger.LogInfo(c, fmt.Sprintf("record consume log: userId=%d, params=%s", userId, common.GetJsonString(params))) |
|
|
username := c.GetString("username") |
|
|
otherStr := common.MapToJsonStr(params.Other) |
|
|
|
|
|
needRecordIp := false |
|
|
if settingMap, err := GetUserSetting(userId, false); err == nil { |
|
|
if settingMap.RecordIpLog { |
|
|
needRecordIp = true |
|
|
} |
|
|
} |
|
|
log := &Log{ |
|
|
UserId: userId, |
|
|
Username: username, |
|
|
CreatedAt: common.GetTimestamp(), |
|
|
Type: LogTypeConsume, |
|
|
Content: params.Content, |
|
|
PromptTokens: params.PromptTokens, |
|
|
CompletionTokens: params.CompletionTokens, |
|
|
TokenName: params.TokenName, |
|
|
ModelName: params.ModelName, |
|
|
Quota: params.Quota, |
|
|
ChannelId: params.ChannelId, |
|
|
TokenId: params.TokenId, |
|
|
UseTime: params.UseTimeSeconds, |
|
|
IsStream: params.IsStream, |
|
|
Group: params.Group, |
|
|
Ip: func() string { |
|
|
if needRecordIp { |
|
|
return c.ClientIP() |
|
|
} |
|
|
return "" |
|
|
}(), |
|
|
Other: otherStr, |
|
|
} |
|
|
err := LOG_DB.Create(log).Error |
|
|
if err != nil { |
|
|
logger.LogError(c, "failed to record log: "+err.Error()) |
|
|
} |
|
|
if common.DataExportEnabled { |
|
|
gopool.Go(func() { |
|
|
LogQuotaData(userId, username, params.ModelName, params.Quota, common.GetTimestamp(), params.PromptTokens+params.CompletionTokens) |
|
|
}) |
|
|
} |
|
|
} |
|
|
|
|
|
func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int, group string) (logs []*Log, total int64, err error) { |
|
|
var tx *gorm.DB |
|
|
if logType == LogTypeUnknown { |
|
|
tx = LOG_DB |
|
|
} else { |
|
|
tx = LOG_DB.Where("logs.type = ?", logType) |
|
|
} |
|
|
|
|
|
if modelName != "" { |
|
|
tx = tx.Where("logs.model_name like ?", modelName) |
|
|
} |
|
|
if username != "" { |
|
|
tx = tx.Where("logs.username = ?", username) |
|
|
} |
|
|
if tokenName != "" { |
|
|
tx = tx.Where("logs.token_name = ?", tokenName) |
|
|
} |
|
|
if startTimestamp != 0 { |
|
|
tx = tx.Where("logs.created_at >= ?", startTimestamp) |
|
|
} |
|
|
if endTimestamp != 0 { |
|
|
tx = tx.Where("logs.created_at <= ?", endTimestamp) |
|
|
} |
|
|
if channel != 0 { |
|
|
tx = tx.Where("logs.channel_id = ?", channel) |
|
|
} |
|
|
if group != "" { |
|
|
tx = tx.Where("logs."+logGroupCol+" = ?", group) |
|
|
} |
|
|
err = tx.Model(&Log{}).Count(&total).Error |
|
|
if err != nil { |
|
|
return nil, 0, err |
|
|
} |
|
|
err = tx.Order("logs.id desc").Limit(num).Offset(startIdx).Find(&logs).Error |
|
|
if err != nil { |
|
|
return nil, 0, err |
|
|
} |
|
|
|
|
|
channelIds := types.NewSet[int]() |
|
|
for _, log := range logs { |
|
|
if log.ChannelId != 0 { |
|
|
channelIds.Add(log.ChannelId) |
|
|
} |
|
|
} |
|
|
|
|
|
if channelIds.Len() > 0 { |
|
|
var channels []struct { |
|
|
Id int `gorm:"column:id"` |
|
|
Name string `gorm:"column:name"` |
|
|
} |
|
|
if err = DB.Table("channels").Select("id, name").Where("id IN ?", channelIds.Items()).Find(&channels).Error; err != nil { |
|
|
return logs, total, err |
|
|
} |
|
|
channelMap := make(map[int]string, len(channels)) |
|
|
for _, channel := range channels { |
|
|
channelMap[channel.Id] = channel.Name |
|
|
} |
|
|
for i := range logs { |
|
|
logs[i].ChannelName = channelMap[logs[i].ChannelId] |
|
|
} |
|
|
} |
|
|
|
|
|
return logs, total, err |
|
|
} |
|
|
|
|
|
func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int, group string) (logs []*Log, total int64, err error) { |
|
|
var tx *gorm.DB |
|
|
if logType == LogTypeUnknown { |
|
|
tx = LOG_DB.Where("logs.user_id = ?", userId) |
|
|
} else { |
|
|
tx = LOG_DB.Where("logs.user_id = ? and logs.type = ?", userId, logType) |
|
|
} |
|
|
|
|
|
if modelName != "" { |
|
|
tx = tx.Where("logs.model_name like ?", modelName) |
|
|
} |
|
|
if tokenName != "" { |
|
|
tx = tx.Where("logs.token_name = ?", tokenName) |
|
|
} |
|
|
if startTimestamp != 0 { |
|
|
tx = tx.Where("logs.created_at >= ?", startTimestamp) |
|
|
} |
|
|
if endTimestamp != 0 { |
|
|
tx = tx.Where("logs.created_at <= ?", endTimestamp) |
|
|
} |
|
|
if group != "" { |
|
|
tx = tx.Where("logs."+logGroupCol+" = ?", group) |
|
|
} |
|
|
err = tx.Model(&Log{}).Count(&total).Error |
|
|
if err != nil { |
|
|
return nil, 0, err |
|
|
} |
|
|
err = tx.Order("logs.id desc").Limit(num).Offset(startIdx).Find(&logs).Error |
|
|
if err != nil { |
|
|
return nil, 0, err |
|
|
} |
|
|
|
|
|
formatUserLogs(logs) |
|
|
return logs, total, err |
|
|
} |
|
|
|
|
|
func SearchAllLogs(keyword string) (logs []*Log, err error) { |
|
|
err = LOG_DB.Where("type = ? or content LIKE ?", keyword, keyword+"%").Order("id desc").Limit(common.MaxRecentItems).Find(&logs).Error |
|
|
return logs, err |
|
|
} |
|
|
|
|
|
func SearchUserLogs(userId int, keyword string) (logs []*Log, err error) { |
|
|
err = LOG_DB.Where("user_id = ? and type = ?", userId, keyword).Order("id desc").Limit(common.MaxRecentItems).Find(&logs).Error |
|
|
formatUserLogs(logs) |
|
|
return logs, err |
|
|
} |
|
|
|
|
|
type Stat struct { |
|
|
Quota int `json:"quota"` |
|
|
Rpm int `json:"rpm"` |
|
|
Tpm int `json:"tpm"` |
|
|
} |
|
|
|
|
|
func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int, group string) (stat Stat) { |
|
|
tx := LOG_DB.Table("logs").Select("sum(quota) quota") |
|
|
|
|
|
|
|
|
rpmTpmQuery := LOG_DB.Table("logs").Select("count(*) rpm, sum(prompt_tokens) + sum(completion_tokens) tpm") |
|
|
|
|
|
if username != "" { |
|
|
tx = tx.Where("username = ?", username) |
|
|
rpmTpmQuery = rpmTpmQuery.Where("username = ?", username) |
|
|
} |
|
|
if tokenName != "" { |
|
|
tx = tx.Where("token_name = ?", tokenName) |
|
|
rpmTpmQuery = rpmTpmQuery.Where("token_name = ?", tokenName) |
|
|
} |
|
|
if startTimestamp != 0 { |
|
|
tx = tx.Where("created_at >= ?", startTimestamp) |
|
|
} |
|
|
if endTimestamp != 0 { |
|
|
tx = tx.Where("created_at <= ?", endTimestamp) |
|
|
} |
|
|
if modelName != "" { |
|
|
tx = tx.Where("model_name like ?", modelName) |
|
|
rpmTpmQuery = rpmTpmQuery.Where("model_name like ?", modelName) |
|
|
} |
|
|
if channel != 0 { |
|
|
tx = tx.Where("channel_id = ?", channel) |
|
|
rpmTpmQuery = rpmTpmQuery.Where("channel_id = ?", channel) |
|
|
} |
|
|
if group != "" { |
|
|
tx = tx.Where(logGroupCol+" = ?", group) |
|
|
rpmTpmQuery = rpmTpmQuery.Where(logGroupCol+" = ?", group) |
|
|
} |
|
|
|
|
|
tx = tx.Where("type = ?", LogTypeConsume) |
|
|
rpmTpmQuery = rpmTpmQuery.Where("type = ?", LogTypeConsume) |
|
|
|
|
|
|
|
|
rpmTpmQuery = rpmTpmQuery.Where("created_at >= ?", time.Now().Add(-60*time.Second).Unix()) |
|
|
|
|
|
|
|
|
tx.Scan(&stat) |
|
|
rpmTpmQuery.Scan(&stat) |
|
|
|
|
|
return stat |
|
|
} |
|
|
|
|
|
func SumUsedToken(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string) (token int) { |
|
|
tx := LOG_DB.Table("logs").Select("ifnull(sum(prompt_tokens),0) + ifnull(sum(completion_tokens),0)") |
|
|
if username != "" { |
|
|
tx = tx.Where("username = ?", username) |
|
|
} |
|
|
if tokenName != "" { |
|
|
tx = tx.Where("token_name = ?", tokenName) |
|
|
} |
|
|
if startTimestamp != 0 { |
|
|
tx = tx.Where("created_at >= ?", startTimestamp) |
|
|
} |
|
|
if endTimestamp != 0 { |
|
|
tx = tx.Where("created_at <= ?", endTimestamp) |
|
|
} |
|
|
if modelName != "" { |
|
|
tx = tx.Where("model_name = ?", modelName) |
|
|
} |
|
|
tx.Where("type = ?", LogTypeConsume).Scan(&token) |
|
|
return token |
|
|
} |
|
|
|
|
|
func DeleteOldLog(ctx context.Context, targetTimestamp int64, limit int) (int64, error) { |
|
|
var total int64 = 0 |
|
|
|
|
|
for { |
|
|
if nil != ctx.Err() { |
|
|
return total, ctx.Err() |
|
|
} |
|
|
|
|
|
result := LOG_DB.Where("created_at < ?", targetTimestamp).Limit(limit).Delete(&Log{}) |
|
|
if nil != result.Error { |
|
|
return total, result.Error |
|
|
} |
|
|
|
|
|
total += result.RowsAffected |
|
|
|
|
|
if result.RowsAffected < int64(limit) { |
|
|
break |
|
|
} |
|
|
} |
|
|
|
|
|
return total, nil |
|
|
} |
|
|
|