Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
232 lines
7.7 KiB
Go
232 lines
7.7 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/model"
|
|
perfmetrics "github.com/QuantumNous/new-api/pkg/perf_metrics"
|
|
"github.com/QuantumNous/new-api/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const requestTimeout = 30 * time.Second
|
|
|
|
type Monitor struct {
|
|
ProbeHistory []service.ProbeHistoryPoint `json:"probe_history"`
|
|
ID string `json:"id"`
|
|
Models []string `json:"models"`
|
|
Alias string `json:"alias,omitempty"`
|
|
Hidden bool `json:"hidden,omitempty"`
|
|
Pelican *service.PelicanSummary `json:"pelican,omitempty"`
|
|
StatusSource string `json:"status_source"`
|
|
StatusCheckedAt int64 `json:"status_checked_at,omitempty"`
|
|
Fingerprint *service.AstraSamplingSummary `json:"fingerprint,omitempty"`
|
|
Metrics *perfmetrics.GroupResult `json:"metrics,omitempty"`
|
|
Name string `json:"name"`
|
|
Uptime float64 `json:"uptime"`
|
|
Status int `json:"status"`
|
|
Group string `json:"group,omitempty"`
|
|
Ping int `json:"ping,omitempty"`
|
|
LastChecked string `json:"last_checked,omitempty"`
|
|
History []MonitorSample `json:"history,omitempty"`
|
|
CacheStats *model.GroupCacheStats `json:"cache_stats,omitempty"`
|
|
ProbeStatus int `json:"probe_status"`
|
|
ChannelDisabled bool `json:"channel_disabled"`
|
|
}
|
|
|
|
type MonitorSample struct {
|
|
Status int `json:"status"`
|
|
Ping int `json:"ping,omitempty"`
|
|
Time string `json:"time"`
|
|
}
|
|
|
|
type UptimeGroupResult struct {
|
|
CategoryName string `json:"categoryName"`
|
|
Monitors []Monitor `json:"monitors"`
|
|
}
|
|
|
|
var groupCacheSnapshot struct {
|
|
sync.Mutex
|
|
updated time.Time
|
|
channels string
|
|
stats map[string]model.GroupCacheStats
|
|
metrics map[string]perfmetrics.GroupResult
|
|
}
|
|
|
|
const requestStatusFreshness = 2 * time.Hour
|
|
|
|
func currentGroupMonitors(ctx context.Context, admin bool) ([]UptimeGroupResult, error) {
|
|
channels, err := model.GetStatusChannels(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
settings, err := model.GetStatusPageGroups(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
models, err := model.GetStatusPageModels(ctx, channels)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
usable := service.GetUserUsableGroups("")
|
|
now := time.Now()
|
|
fingerprints, fingerprintErr := service.AstraSamplingSummaries(ctx, now, settings)
|
|
if fingerprintErr != nil {
|
|
return nil, fingerprintErr
|
|
}
|
|
detections, detectionErr := service.GroupDetectionSummaries(ctx, now, fingerprints, settings)
|
|
if detectionErr != nil {
|
|
return nil, detectionErr
|
|
}
|
|
active := make(map[string]bool)
|
|
order := make([]string, 0)
|
|
for _, channel := range channels {
|
|
for _, group := range strings.Split(channel.Group, ",") {
|
|
group = strings.TrimSpace(group)
|
|
if _, visible := usable[group]; (!visible || settings[group].Hidden) && !admin {
|
|
continue
|
|
}
|
|
if _, exists := active[group]; !exists {
|
|
order = append(order, group)
|
|
}
|
|
active[group] = active[group] || channel.Status == common.ChannelStatusEnabled
|
|
}
|
|
}
|
|
encoded, err := common.Marshal(channels)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
groupCacheSnapshot.Lock()
|
|
defer groupCacheSnapshot.Unlock()
|
|
if time.Since(groupCacheSnapshot.updated) >= time.Minute || groupCacheSnapshot.channels != string(encoded) {
|
|
now := time.Now()
|
|
stats, statsErr := model.GetGroupCacheStats(ctx, channels, now.Add(-24*time.Hour), now)
|
|
if statsErr != nil {
|
|
return nil, statsErr
|
|
}
|
|
groupCacheSnapshot.stats = stats
|
|
names := []string{}
|
|
if err := model.DB.WithContext(ctx).Model(&model.Ability{}).Where("enabled = ?", true).Distinct().Pluck("model", &names).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
performance, metricsErr := perfmetrics.QueryGroups(names, 24)
|
|
if metricsErr != nil {
|
|
return nil, metricsErr
|
|
}
|
|
groupCacheSnapshot.metrics = make(map[string]perfmetrics.GroupResult)
|
|
for _, group := range performance.Groups {
|
|
groupCacheSnapshot.metrics[group.Group] = group
|
|
}
|
|
groupCacheSnapshot.updated = now
|
|
groupCacheSnapshot.channels = string(encoded)
|
|
}
|
|
successes := perfmetrics.LatestUserSuccess(ctx, order)
|
|
monitors := make([]Monitor, 0, len(order))
|
|
for _, group := range order {
|
|
monitor := Monitor{ID: model.DetectionSecretHash(group)[:16], Name: group, Status: -1, Models: models[group]}
|
|
if monitor.Models == nil {
|
|
monitor.Models = []string{}
|
|
}
|
|
if settings[group].Alias != "" {
|
|
monitor.Name = settings[group].Alias
|
|
}
|
|
if admin {
|
|
monitor.Alias, monitor.Hidden = settings[group].Alias, settings[group].Hidden
|
|
}
|
|
if admin {
|
|
monitor.Group = group
|
|
}
|
|
monitor.Fingerprint = fingerprints[group]
|
|
if monitor.Fingerprint == nil {
|
|
monitor.Fingerprint = &service.AstraSamplingSummary{Enabled: service.AstraSamplingEnabled(), Available: fingerprintErr == nil, History: []service.AstraSamplingHistory{}}
|
|
}
|
|
monitor.ProbeStatus = monitor.Status
|
|
monitor.ChannelDisabled = !active[group]
|
|
if !active[group] {
|
|
monitor.Status = 0
|
|
}
|
|
stats := groupCacheSnapshot.stats[group]
|
|
monitor.CacheStats = &stats
|
|
applyRequestMetrics(&monitor, groupCacheSnapshot.metrics[group])
|
|
if !admin && monitor.Metrics != nil {
|
|
monitor.Metrics.Group = monitor.Name
|
|
}
|
|
monitor.Pelican = detections[group].Pelican
|
|
monitor.StatusSource = "requests"
|
|
if monitor.ChannelDisabled {
|
|
monitor.StatusSource = "disabled"
|
|
} else if monitor.Status < 0 {
|
|
monitor.Status = detections[group].Status
|
|
monitor.StatusSource = "detection"
|
|
monitor.StatusCheckedAt = detections[group].At
|
|
}
|
|
monitor.ProbeHistory = detections[group].ProbeHistory
|
|
applyUserSuccess(&monitor, successes[group], detections[group].At, now.Unix())
|
|
monitors = append(monitors, monitor)
|
|
}
|
|
return []UptimeGroupResult{{CategoryName: "Groups", Monitors: monitors}}, nil
|
|
}
|
|
|
|
func GetGroupStatus(c *gin.Context) {
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
|
|
defer cancel()
|
|
|
|
results, err := currentGroupMonitors(ctx, false)
|
|
if err != nil {
|
|
common.SysError("group status statistics: " + err.Error())
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"success": false, "message": "Status data is temporarily unavailable"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "", "data": results})
|
|
}
|
|
|
|
func requestStatus(rate float64) int {
|
|
if rate >= 90 {
|
|
return 1
|
|
}
|
|
if rate >= 70 {
|
|
return 2
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func applyUserSuccess(monitor *Monitor, success, probe, now int64) {
|
|
if !monitor.ChannelDisabled && success > 0 && success >= now-900 && success >= probe {
|
|
monitor.Status = 1
|
|
monitor.StatusSource = "requests"
|
|
monitor.StatusCheckedAt = success
|
|
}
|
|
}
|
|
|
|
func applyRequestMetrics(monitor *Monitor, metrics perfmetrics.GroupResult) {
|
|
monitor.Status = -1
|
|
monitor.Uptime = 0
|
|
monitor.Ping = 0
|
|
monitor.LastChecked = ""
|
|
monitor.History = nil
|
|
if len(metrics.Series) > 0 {
|
|
monitor.Metrics = &metrics
|
|
monitor.Uptime = metrics.SuccessRate / 100
|
|
latest := metrics.Series[len(metrics.Series)-1]
|
|
if time.Since(time.Unix(latest.Ts, 0)) <= requestStatusFreshness {
|
|
monitor.Status = requestStatus(latest.SuccessRate)
|
|
}
|
|
for _, point := range metrics.Series {
|
|
monitor.History = append(monitor.History, MonitorSample{
|
|
Status: requestStatus(point.SuccessRate), Ping: int(point.AvgTtftMs),
|
|
Time: time.Unix(point.Ts, 0).UTC().Format("2006-01-02 15:04:05"),
|
|
})
|
|
}
|
|
monitor.LastChecked = monitor.History[len(monitor.History)-1].Time
|
|
}
|
|
if monitor.ChannelDisabled {
|
|
monitor.Status = 0
|
|
}
|
|
}
|