Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions controller/channel-test.go
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,29 @@ func TestAllChannels(c *gin.Context) {
})
}

// MonitorTestResult holds the result of a channel test for the group monitor module.
type MonitorTestResult struct {
Success bool
LatencyMs int64
ErrorMsg string
}

// TestChannelForMonitor runs a non-streaming test on the given channel and returns a simplified result.
func TestChannelForMonitor(channel *model.Channel, testModel string) MonitorTestResult {
tik := time.Now()
result := testChannel(channel, testModel, "", false)
latencyMs := time.Since(tik).Milliseconds()
r := MonitorTestResult{LatencyMs: latencyMs}
if result.localErr != nil {
r.ErrorMsg = result.localErr.Error()
} else if result.newAPIError != nil {
r.ErrorMsg = result.newAPIError.Error()
} else {
r.Success = true
}
return r
}

var autoTestChannelsOnce sync.Once

func AutomaticallyTestChannels() {
Expand Down
5 changes: 5 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/oauth"
"github.com/QuantumNous/new-api/router"
"github.com/QuantumNous/new-api/modules/group_monitor"
"github.com/QuantumNous/new-api/service"
_ "github.com/QuantumNous/new-api/setting/performance_setting"
"github.com/QuantumNous/new-api/setting/ratio_setting"
Expand Down Expand Up @@ -104,6 +105,7 @@ func main() {
}

go controller.AutomaticallyTestChannels()
go group_monitor.AutomaticallyGroupMonitor()

// Codex credential auto-refresh check every 10 minutes, refresh when expires within 1 day
service.StartCodexCredentialAutoRefreshTask()
Expand Down Expand Up @@ -299,5 +301,8 @@ func InitResources() error {
// Don't return error, custom OAuth is not critical
}

// Migrate group monitor tables
group_monitor.Migrate()

Comment on lines +304 to +306
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Migration error is silently discarded.

group_monitor.Migrate() returns an error that is ignored. If the migration fails, the background monitor goroutine (Line 108) will encounter runtime errors querying non-existent tables. Other migrations in this function propagate errors.

Suggested fix
 	// Migrate group monitor tables
-	group_monitor.Migrate()
-
+	if err = group_monitor.Migrate(); err != nil {
+		common.SysError("failed to migrate group monitor tables: " + err.Error())
+		// Non-fatal: log but don't block startup
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Migrate group monitor tables
group_monitor.Migrate()
// Migrate group monitor tables
if err = group_monitor.Migrate(); err != nil {
common.SysError("failed to migrate group monitor tables: " + err.Error())
// Non-fatal: log but don't block startup
}
🤖 Prompt for AI Agents
In `@main.go` around lines 304 - 306, The call to group_monitor.Migrate()
currently ignores its returned error; capture its error and handle it the same
way as the other migrations (e.g., if err := group_monitor.Migrate(); err != nil
{ return fmt.Errorf("group_monitor.Migrate: %w", err) } or log/exit
consistently) so failures are propagated before starting the background monitor
goroutine (which queries those tables) and prevent runtime panics; update the
invocation in main.go to check the error and propagate or fail fast (matching
the existing error handling pattern in this function).

return nil
}
181 changes: 181 additions & 0 deletions modules/group_monitor/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package group_monitor

import (
"net/http"
"strconv"

"github.com/QuantumNous/new-api/common"

"github.com/gin-gonic/gin"
)

// GetGroupMonitorLogs 分页查询监控日志(管理员)
func GetGroupMonitorLogsHandler(c *gin.Context) {
pageInfo := common.GetPageQuery(c)
groupName := c.Query("group")
startTs, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
endTs, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)

logs, total, err := GetGroupMonitorLogs(groupName, startTs, endTs, pageInfo.GetStartIdx(), pageInfo.GetPageSize())
if err != nil {
common.ApiError(c, err)
return
}
pageInfo.SetTotal(int(total))
pageInfo.SetItems(logs)
common.ApiSuccess(c, pageInfo)
}

// GetGroupMonitorLatestHandler 获取所有分组最新状态(管理员)
func GetGroupMonitorLatestHandler(c *gin.Context) {
logs, err := GetGroupMonitorLatest()
if err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, logs)
}

// GetGroupMonitorStatsHandler 获取聚合统计(管理员)
func GetGroupMonitorStatsHandler(c *gin.Context) {
startTs, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
endTs, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)

// 默认查询最近 1 小时
if startTs == 0 {
startTs = common.GetTimestamp() - 3600
}

stats, err := GetGroupMonitorStats(startTs, endTs)
if err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, stats)
}

// GetGroupMonitorTimeSeriesHandler 获取时间序列数据(趋势图)
func GetGroupMonitorTimeSeriesHandler(c *gin.Context) {
groupName := c.Query("group")
startTs, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
endTs, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)

// 默认最近 1 小时
if startTs == 0 {
startTs = common.GetTimestamp() - 3600
}

logs, err := GetGroupMonitorTimeSeries(groupName, startTs, endTs)
if err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, logs)
}

// GetGroupMonitorConfigsHandler 获取所有分组监控配置(管理员)
func GetGroupMonitorConfigsHandler(c *gin.Context) {
configs, err := GetAllGroupMonitorConfigs()
if err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, configs)
}

// SaveGroupMonitorConfigHandler 保存分组监控配置(管理员)
func SaveGroupMonitorConfigHandler(c *gin.Context) {
var cfg GroupMonitorConfig
if err := c.ShouldBindJSON(&cfg); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "invalid request body",
})
return
}

if cfg.GroupName == "" {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "group_name is required",
})
return
}

if err := SaveGroupMonitorConfig(&cfg); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, nil)
}

// DeleteGroupMonitorConfigHandler 删除分组监控配置(管理员)
func DeleteGroupMonitorConfigHandler(c *gin.Context) {
groupName := c.Param("group")
if groupName == "" {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "group name is required",
})
return
}

if err := DeleteGroupMonitorConfig(groupName); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, nil)
}

// GetGroupMonitorStatusHandler 用户可见的简化状态
func GetGroupMonitorStatusHandler(c *gin.Context) {
// 获取最近 1 小时的聚合统计
startTs := common.GetTimestamp() - 3600
stats, err := GetGroupMonitorStats(startTs, 0)
if err != nil {
common.ApiError(c, err)
return
}

// 获取每个分组的最新记录
latest, err := GetGroupMonitorLatest()
if err != nil {
common.ApiError(c, err)
return
}

type GroupStatus struct {
GroupName string `json:"group_name"`
LatestLatency int64 `json:"latest_latency"`
LatestSuccess bool `json:"latest_success"`
LatestTime int64 `json:"latest_time"`
AvgLatency float64 `json:"avg_latency"`
Availability float64 `json:"availability"` // 百分比
TotalChecks int64 `json:"total_checks"`
}

// 构建 stats map
statsMap := make(map[string]*GroupMonitorStat)
for i := range stats {
statsMap[stats[i].GroupName] = &stats[i]
}

var result []GroupStatus
for _, log := range latest {
status := GroupStatus{
GroupName: log.GroupName,
LatestLatency: log.LatencyMs,
LatestSuccess: log.Success,
LatestTime: log.CreatedAt,
}
if stat, ok := statsMap[log.GroupName]; ok {
status.AvgLatency = stat.AvgLatency
status.TotalChecks = stat.TotalCount
if stat.TotalCount > 0 {
status.Availability = float64(stat.SuccessCount) / float64(stat.TotalCount) * 100
}
}
result = append(result, status)
}
common.ApiSuccess(c, result)
}
10 changes: 10 additions & 0 deletions modules/group_monitor/migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package group_monitor

import (
"github.com/QuantumNous/new-api/model"
)

// Migrate 执行分组监控模块的数据库迁移
func Migrate() error {
return model.DB.AutoMigrate(&GroupMonitorLog{}, &GroupMonitorConfig{})
}
Loading