* feat: 添加请求和响应体日志记录功能 - 在 RequestLog 模型中添加 request_body 和 response_body 字段用于存储完整的请求和响应内容 - 修改响应处理器以捕获和返回响应体内容,支持流式和非流式响应 - 更新代理服务器逻辑,在请求日志中记录请求体和响应体 - 在前端日志表格中添加详情查看功能,支持查看完整的请求和响应内容 - 提供 JSON 格式化显示和语法高亮,提升日志查看体验 * feat: 添加是否记录响应和请求体按钮 * fix: 修改请求和响应体记录选项的默认值为false * feat: 添加分组级请求体日志记录控制功能 - 分组 - 高级配置 - 分组配置添加"禁用请求和响应体日志记录"选项 (disable_request_body_logging),覆盖系统设置 * refactor: 重构请求体响应体记录分组级别逻辑 - 不再单独处理分组设置,系统和分组使用同一个配置key - 具体逻辑: - 系统启用 + 分组不做配置 = 记录 - 系统启用 + 分组启用 = 记录 - 系统启用 + 分组禁用 = 未记录请求内容(此分组已禁用请求体记录功能) - 系统禁用 + 分组开启 = 未记录请求内容(系统设置中已关闭请求体记录功能) - 系统禁用 = 未记录请求内容(系统设置中已关闭请求体记录功能) * feat: 优化日志详情记录 * fix: space * fix: code * feat: 记录重试日志 * feat: 内容复制 * feat: 删除响应内容记录 * feat: 优化请求日志记录 * feat: 统计过滤重试日志 * feat: 优化日志详情记录 * feat: 移除url的key记录 * feat: 移除请求key * feat: remove highlight.js * feat: format * fix: 恢复误删的keydelete --------- Co-authored-by: limliu <lim.liu.01@outlook.com>
223 lines
6.5 KiB
Go
223 lines
6.5 KiB
Go
package handler
|
|
|
|
import (
|
|
app_errors "gpt-load/internal/errors"
|
|
"gpt-load/internal/models"
|
|
"gpt-load/internal/response"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// Stats Get dashboard statistics
|
|
func (s *Server) Stats(c *gin.Context) {
|
|
var activeKeys, invalidKeys int64
|
|
s.DB.Model(&models.APIKey{}).Where("status = ?", models.KeyStatusActive).Count(&activeKeys)
|
|
s.DB.Model(&models.APIKey{}).Where("status = ?", models.KeyStatusInvalid).Count(&invalidKeys)
|
|
|
|
now := time.Now()
|
|
rpmStats, err := s.getRPMStats(now)
|
|
if err != nil {
|
|
response.Error(c, app_errors.NewAPIError(app_errors.ErrDatabase, "failed to get rpm stats"))
|
|
return
|
|
}
|
|
twentyFourHoursAgo := now.Add(-24 * time.Hour)
|
|
fortyEightHoursAgo := now.Add(-48 * time.Hour)
|
|
|
|
currentPeriod, err := s.getHourlyStats(twentyFourHoursAgo, now)
|
|
if err != nil {
|
|
response.Error(c, app_errors.NewAPIError(app_errors.ErrDatabase, "failed to get current period stats"))
|
|
return
|
|
}
|
|
previousPeriod, err := s.getHourlyStats(fortyEightHoursAgo, twentyFourHoursAgo)
|
|
if err != nil {
|
|
response.Error(c, app_errors.NewAPIError(app_errors.ErrDatabase, "failed to get previous period stats"))
|
|
return
|
|
}
|
|
|
|
// 计算请求量趋势
|
|
reqTrend := 0.0
|
|
reqTrendIsGrowth := true
|
|
if previousPeriod.TotalRequests > 0 {
|
|
// 有前期数据,计算百分比变化
|
|
reqTrend = (float64(currentPeriod.TotalRequests-previousPeriod.TotalRequests) / float64(previousPeriod.TotalRequests)) * 100
|
|
reqTrendIsGrowth = reqTrend >= 0
|
|
} else if currentPeriod.TotalRequests > 0 {
|
|
// 前期无数据,当前有数据,视为100%增长
|
|
reqTrend = 100.0
|
|
reqTrendIsGrowth = true
|
|
} else {
|
|
// 前期和当前都无数据
|
|
reqTrend = 0.0
|
|
reqTrendIsGrowth = true
|
|
}
|
|
|
|
// 计算当前和前期错误率
|
|
currentErrorRate := 0.0
|
|
if currentPeriod.TotalRequests > 0 {
|
|
currentErrorRate = (float64(currentPeriod.TotalFailures) / float64(currentPeriod.TotalRequests)) * 100
|
|
}
|
|
|
|
previousErrorRate := 0.0
|
|
if previousPeriod.TotalRequests > 0 {
|
|
previousErrorRate = (float64(previousPeriod.TotalFailures) / float64(previousPeriod.TotalRequests)) * 100
|
|
}
|
|
|
|
// 计算错误率趋势
|
|
errorRateTrend := 0.0
|
|
errorRateTrendIsGrowth := false
|
|
if previousPeriod.TotalRequests > 0 {
|
|
// 有前期数据,计算百分点差异
|
|
errorRateTrend = currentErrorRate - previousErrorRate
|
|
errorRateTrendIsGrowth = errorRateTrend < 0 // 错误率下降是好事
|
|
} else if currentPeriod.TotalRequests > 0 {
|
|
// 前期无数据,当前有数据
|
|
errorRateTrend = currentErrorRate // 显示当前错误率
|
|
errorRateTrendIsGrowth = false // 有错误是坏事(如果错误率>0)
|
|
if currentErrorRate == 0 {
|
|
errorRateTrendIsGrowth = true // 如果当前无错误,标记为正面
|
|
}
|
|
} else {
|
|
// 都无数据
|
|
errorRateTrend = 0.0
|
|
errorRateTrendIsGrowth = true
|
|
}
|
|
|
|
stats := models.DashboardStatsResponse{
|
|
KeyCount: models.StatCard{
|
|
Value: float64(activeKeys),
|
|
SubValue: invalidKeys,
|
|
SubValueTip: "无效密钥数量",
|
|
},
|
|
RPM: rpmStats,
|
|
RequestCount: models.StatCard{
|
|
Value: float64(currentPeriod.TotalRequests),
|
|
Trend: reqTrend,
|
|
TrendIsGrowth: reqTrendIsGrowth,
|
|
},
|
|
ErrorRate: models.StatCard{
|
|
Value: currentErrorRate,
|
|
Trend: errorRateTrend,
|
|
TrendIsGrowth: errorRateTrendIsGrowth,
|
|
},
|
|
}
|
|
|
|
response.Success(c, stats)
|
|
}
|
|
|
|
// Chart Get dashboard chart data
|
|
func (s *Server) Chart(c *gin.Context) {
|
|
groupID := c.Query("groupId")
|
|
|
|
now := time.Now()
|
|
endHour := now.Truncate(time.Hour)
|
|
startHour := endHour.Add(-23 * time.Hour)
|
|
|
|
var hourlyStats []models.GroupHourlyStat
|
|
query := s.DB.Where("time >= ? AND time < ?", startHour, endHour.Add(time.Hour))
|
|
if groupID != "" {
|
|
query = query.Where("group_id = ?", groupID)
|
|
}
|
|
if err := query.Order("time asc").Find(&hourlyStats).Error; err != nil {
|
|
response.Error(c, app_errors.NewAPIError(app_errors.ErrDatabase, "failed to get chart data"))
|
|
return
|
|
}
|
|
|
|
statsByHour := make(map[time.Time]map[string]int64)
|
|
for _, stat := range hourlyStats {
|
|
hour := stat.Time.Local().Truncate(time.Hour)
|
|
if _, ok := statsByHour[hour]; !ok {
|
|
statsByHour[hour] = make(map[string]int64)
|
|
}
|
|
statsByHour[hour]["success"] += stat.SuccessCount
|
|
statsByHour[hour]["failure"] += stat.FailureCount
|
|
}
|
|
|
|
var labels []string
|
|
var successData, failureData []int64
|
|
|
|
for i := range 24 {
|
|
hour := startHour.Add(time.Duration(i) * time.Hour)
|
|
labels = append(labels, hour.Format(time.RFC3339))
|
|
|
|
if data, ok := statsByHour[hour]; ok {
|
|
successData = append(successData, data["success"])
|
|
failureData = append(failureData, data["failure"])
|
|
} else {
|
|
successData = append(successData, 0)
|
|
failureData = append(failureData, 0)
|
|
}
|
|
}
|
|
|
|
chartData := models.ChartData{
|
|
Labels: labels,
|
|
Datasets: []models.ChartDataset{
|
|
{
|
|
Label: "成功请求",
|
|
Data: successData,
|
|
Color: "rgba(10, 200, 110, 1)",
|
|
},
|
|
{
|
|
Label: "失败请求",
|
|
Data: failureData,
|
|
Color: "rgba(255, 70, 70, 1)",
|
|
},
|
|
},
|
|
}
|
|
|
|
response.Success(c, chartData)
|
|
}
|
|
|
|
type hourlyStatResult struct {
|
|
TotalRequests int64
|
|
TotalFailures int64
|
|
}
|
|
|
|
func (s *Server) getHourlyStats(startTime, endTime time.Time) (hourlyStatResult, error) {
|
|
var result hourlyStatResult
|
|
err := s.DB.Model(&models.GroupHourlyStat{}).
|
|
Select("sum(success_count) + sum(failure_count) as total_requests, sum(failure_count) as total_failures").
|
|
Where("time >= ? AND time < ?", startTime, endTime).
|
|
Scan(&result).Error
|
|
return result, err
|
|
}
|
|
|
|
type rpmStatResult struct {
|
|
CurrentRequests int64
|
|
PreviousRequests int64
|
|
}
|
|
|
|
func (s *Server) getRPMStats(now time.Time) (models.StatCard, error) {
|
|
tenMinutesAgo := now.Add(-10 * time.Minute)
|
|
twentyMinutesAgo := now.Add(-20 * time.Minute)
|
|
|
|
var result rpmStatResult
|
|
err := s.DB.Model(&models.RequestLog{}).
|
|
Select("count(case when timestamp >= ? then 1 end) as current_requests, count(case when timestamp >= ? and timestamp < ? then 1 end) as previous_requests", tenMinutesAgo, twentyMinutesAgo, tenMinutesAgo).
|
|
Where("timestamp >= ? AND request_type = ?", twentyMinutesAgo, models.RequestTypeFinal).
|
|
Scan(&result).Error
|
|
|
|
if err != nil {
|
|
return models.StatCard{}, err
|
|
}
|
|
|
|
currentRPM := float64(result.CurrentRequests) / 10.0
|
|
previousRPM := float64(result.PreviousRequests) / 10.0
|
|
|
|
rpmTrend := 0.0
|
|
rpmTrendIsGrowth := true
|
|
if previousRPM > 0 {
|
|
rpmTrend = (currentRPM - previousRPM) / previousRPM * 100
|
|
rpmTrendIsGrowth = rpmTrend >= 0
|
|
} else if currentRPM > 0 {
|
|
rpmTrend = 100.0
|
|
rpmTrendIsGrowth = true
|
|
}
|
|
|
|
return models.StatCard{
|
|
Value: currentRPM,
|
|
Trend: rpmTrend,
|
|
TrendIsGrowth: rpmTrendIsGrowth,
|
|
}, nil
|
|
}
|