diff --git a/backend/api/handlers.go b/backend/api/handlers.go index 8682dc5..d661583 100644 --- a/backend/api/handlers.go +++ b/backend/api/handlers.go @@ -140,6 +140,7 @@ func createRequestLog(apiKeyID uint, virtualModelName string, backendModel *mode APIKeyID: apiKeyID, VirtualModelName: virtualModelName, BackendModelName: backendModel.Name, + ProviderName: backendModel.Provider.Name, RequestTimestamp: requestTimestamp, ResponseTimestamp: responseTimestamp, RequestTokens: requestTokenCount, @@ -172,3 +173,155 @@ func copyResponseHeaders(c *gin.Context, resp *http.Response) { } } } + +// GetRequestLogsHandler 获取请求日志列表 +func (h *APIHandler) GetRequestLogsHandler(c *gin.Context) { + // 获取分页参数 + page := 1 + pageSize := 20 + if pageParam := c.Query("page"); pageParam != "" { + fmt.Sscanf(pageParam, "%d", &page) + } + if pageSizeParam := c.Query("page_size"); pageSizeParam != "" { + fmt.Sscanf(pageSizeParam, "%d", &pageSize) + } + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + + // 构建查询 + query := h.DB.Model(&models.RequestLog{}) + + // 按时间范围过滤 + if startTime := c.Query("start_time"); startTime != "" { + if t, err := time.Parse(time.RFC3339, startTime); err == nil { + query = query.Where("request_timestamp >= ?", t) + } + } + if endTime := c.Query("end_time"); endTime != "" { + if t, err := time.Parse(time.RFC3339, endTime); err == nil { + query = query.Where("request_timestamp <= ?", t) + } + } + + // 按虚拟模型名称过滤 + if virtualModel := c.Query("virtual_model"); virtualModel != "" { + query = query.Where("virtual_model_name = ?", virtualModel) + } + + // 按后端模型名称过滤 + if backendModel := c.Query("backend_model"); backendModel != "" { + query = query.Where("backend_model_name = ?", backendModel) + } + + // 获取总数 + var total int64 + if err := query.Count(&total).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to count logs"}) + return + } + + // 获取日志列表 + var logs []models.RequestLog + offset := (page - 1) * pageSize + if err := query.Order("request_timestamp DESC").Limit(pageSize).Offset(offset).Find(&logs).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch logs"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "logs": logs, + "total": total, + "page": page, + "page_size": pageSize, + "total_pages": (total + int64(pageSize) - 1) / int64(pageSize), + }) +} + +// GetRequestLogStatsHandler 获取请求日志统计信息 +func (h *APIHandler) GetRequestLogStatsHandler(c *gin.Context) { + // 获取时间范围参数 + var startTime, endTime time.Time + var err error + + if startTimeParam := c.Query("start_time"); startTimeParam != "" { + startTime, err = time.Parse(time.RFC3339, startTimeParam) + if err != nil { + // 默认最近7天 + startTime = time.Now().AddDate(0, 0, -7) + } + } else { + startTime = time.Now().AddDate(0, 0, -7) + } + + if endTimeParam := c.Query("end_time"); endTimeParam != "" { + endTime, err = time.Parse(time.RFC3339, endTimeParam) + if err != nil { + endTime = time.Now() + } + } else { + endTime = time.Now() + } + + // 统计总请求数 + var totalRequests int64 + h.DB.Model(&models.RequestLog{}). + Where("request_timestamp BETWEEN ? AND ?", startTime, endTime). + Count(&totalRequests) + + // 统计总成本 + var totalCost float64 + h.DB.Model(&models.RequestLog{}). + Where("request_timestamp BETWEEN ? AND ?", startTime, endTime). + Select("COALESCE(SUM(cost), 0)"). + Scan(&totalCost) + + // 统计总token数 + var totalTokens struct { + RequestTokens int64 + ResponseTokens int64 + } + h.DB.Model(&models.RequestLog{}). + Where("request_timestamp BETWEEN ? AND ?", startTime, endTime). + Select("COALESCE(SUM(request_tokens), 0) as request_tokens, COALESCE(SUM(response_tokens), 0) as response_tokens"). + Scan(&totalTokens) + + // 按虚拟模型统计 + type ModelStats struct { + ModelName string `json:"model_name"` + Count int64 `json:"count"` + TotalCost float64 `json:"total_cost"` + } + var virtualModelStats []ModelStats + h.DB.Model(&models.RequestLog{}). + Where("request_timestamp BETWEEN ? AND ?", startTime, endTime). + Select("virtual_model_name as model_name, COUNT(*) as count, COALESCE(SUM(cost), 0) as total_cost"). + Group("virtual_model_name"). + Order("count DESC"). + Limit(10). + Scan(&virtualModelStats) + + // 按后端模型统计 + var backendModelStats []ModelStats + h.DB.Model(&models.RequestLog{}). + Where("request_timestamp BETWEEN ? AND ?", startTime, endTime). + Select("backend_model_name as model_name, COUNT(*) as count, COALESCE(SUM(cost), 0) as total_cost"). + Group("backend_model_name"). + Order("count DESC"). + Limit(10). + Scan(&backendModelStats) + + c.JSON(http.StatusOK, gin.H{ + "start_time": startTime, + "end_time": endTime, + "total_requests": totalRequests, + "total_cost": totalCost, + "total_request_tokens": totalTokens.RequestTokens, + "total_response_tokens": totalTokens.ResponseTokens, + "virtual_model_stats": virtualModelStats, + "backend_model_stats": backendModelStats, + }) +} diff --git a/backend/internal/models/schema.go b/backend/internal/models/schema.go index 012cf3c..0a7064e 100644 --- a/backend/internal/models/schema.go +++ b/backend/internal/models/schema.go @@ -54,6 +54,7 @@ type BackendModel struct { type RequestLog struct { gorm.Model APIKeyID uint `gorm:"index"` // API密钥ID + ProviderName string `gorm:"index"` // 服务商名称 VirtualModelName string `gorm:"index"` // 虚拟模型名称 BackendModelName string `gorm:"index"` // 后端模型名称 RequestTimestamp time.Time `gorm:"index;not null"` // 请求时间戳 diff --git a/backend/main.go b/backend/main.go index 696ab94..0859d56 100644 --- a/backend/main.go +++ b/backend/main.go @@ -72,6 +72,10 @@ func main() { api_.GET("/virtual-models/:id", handler.GetVirtualModelHandler) api_.PUT("/virtual-models/:id", handler.UpdateVirtualModelHandler) api_.DELETE("/virtual-models/:id", handler.DeleteVirtualModelHandler) + + // Request Logs + api_.GET("/logs", handler.GetRequestLogsHandler) + api_.GET("/logs/stats", handler.GetRequestLogStatsHandler) } // 启动HTTP服务器 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 732da88..cb3a957 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,17 +2,26 @@ import React from 'react'; import PageLayout from './components/layout/PageLayout'; import ProvidersPage from './features/providers'; import VirtualModelsPage from './features/virtual-models'; +import LogsPage from './features/logs'; import './App.css'; function App() { - return ( - -
- - -
-
- ); + const tabs = [ + { + label: '提供商管理', + content: + }, + { + label: '虚拟模型', + content: + }, + { + label: '请求日志', + content: + } + ]; + + return ; } export default App; diff --git a/frontend/src/components/layout/PageLayout.jsx b/frontend/src/components/layout/PageLayout.jsx index 2d0c60f..c869085 100644 --- a/frontend/src/components/layout/PageLayout.jsx +++ b/frontend/src/components/layout/PageLayout.jsx @@ -1,12 +1,39 @@ -import React from 'react'; +import React, { useState } from 'react'; + +const PageLayout = ({ tabs }) => { + const [activeTab, setActiveTab] = useState(0); -const PageLayout = ({ children }) => { return (
-
-

AI Gateway Config

+
+

AI Gateway Config

+ + {/* 标签页导航 */} +
+ +
-
{children}
+ + {/* 标签页内容 */} +
+ {tabs[activeTab]?.content} +
); };