295 lines
8.1 KiB
Go
295 lines
8.1 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"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"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
const (
|
|
requestTimeout = 30 * time.Second
|
|
httpTimeout = 10 * time.Second
|
|
uptimeKeySuffix = "_24"
|
|
apiStatusPath = "/api/status-page/"
|
|
apiHeartbeatPath = "/api/status-page/heartbeat/"
|
|
)
|
|
|
|
type Monitor struct {
|
|
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"`
|
|
}
|
|
|
|
func getAndDecode(ctx context.Context, client *http.Client, url string, dest any) error {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return errors.New("non-200 status")
|
|
}
|
|
|
|
return common.DecodeJson(resp.Body, dest)
|
|
}
|
|
|
|
func fetchGroupData(ctx context.Context, client *http.Client, groupConfig map[string]any) UptimeGroupResult {
|
|
url, _ := groupConfig["url"].(string)
|
|
slug, _ := groupConfig["slug"].(string)
|
|
categoryName, _ := groupConfig["categoryName"].(string)
|
|
|
|
result := UptimeGroupResult{
|
|
CategoryName: categoryName,
|
|
Monitors: []Monitor{},
|
|
}
|
|
|
|
if url == "" || slug == "" {
|
|
return result
|
|
}
|
|
|
|
baseURL := strings.TrimSuffix(url, "/")
|
|
|
|
var statusData struct {
|
|
PublicGroupList []struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
MonitorList []struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
} `json:"monitorList"`
|
|
} `json:"publicGroupList"`
|
|
}
|
|
|
|
var heartbeatData struct {
|
|
HeartbeatList map[string][]struct {
|
|
Status int `json:"status"`
|
|
Ping int `json:"ping"`
|
|
Time string `json:"time"`
|
|
} `json:"heartbeatList"`
|
|
UptimeList map[string]float64 `json:"uptimeList"`
|
|
}
|
|
|
|
g, gCtx := errgroup.WithContext(ctx)
|
|
g.Go(func() error {
|
|
return getAndDecode(gCtx, client, baseURL+apiStatusPath+slug, &statusData)
|
|
})
|
|
g.Go(func() error {
|
|
return getAndDecode(gCtx, client, baseURL+apiHeartbeatPath+slug, &heartbeatData)
|
|
})
|
|
|
|
if g.Wait() != nil {
|
|
return result
|
|
}
|
|
|
|
for _, pg := range statusData.PublicGroupList {
|
|
if len(pg.MonitorList) == 0 {
|
|
continue
|
|
}
|
|
|
|
for _, m := range pg.MonitorList {
|
|
monitor := Monitor{
|
|
Name: m.Name,
|
|
Group: pg.Name,
|
|
Status: -1,
|
|
}
|
|
|
|
monitorID := strconv.Itoa(m.ID)
|
|
|
|
if uptime, exists := heartbeatData.UptimeList[monitorID+uptimeKeySuffix]; exists {
|
|
monitor.Uptime = uptime
|
|
}
|
|
|
|
if heartbeats, exists := heartbeatData.HeartbeatList[monitorID]; exists && len(heartbeats) > 0 {
|
|
latest := heartbeats[len(heartbeats)-1]
|
|
monitor.Status = latest.Status
|
|
monitor.Ping = latest.Ping
|
|
monitor.LastChecked = latest.Time
|
|
monitor.History = make([]MonitorSample, 0, len(heartbeats))
|
|
for _, heartbeat := range heartbeats {
|
|
monitor.History = append(monitor.History, MonitorSample{
|
|
Status: heartbeat.Status,
|
|
Ping: heartbeat.Ping,
|
|
Time: heartbeat.Time,
|
|
})
|
|
}
|
|
}
|
|
|
|
result.Monitors = append(result.Monitors, monitor)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
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, results []UptimeGroupResult) ([]UptimeGroupResult, error) {
|
|
channels, err := model.GetStatusChannels(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
usable := service.GetUserUsableGroups("")
|
|
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 {
|
|
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{}
|
|
for _, item := range model.GetPricing() {
|
|
names = append(names, item.ModelName)
|
|
}
|
|
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)
|
|
}
|
|
byName := make(map[string]Monitor)
|
|
for _, result := range results {
|
|
for _, monitor := range result.Monitors {
|
|
if previous, exists := byName[monitor.Name]; !exists || (monitor.Status == 1 && previous.Status != 1) {
|
|
byName[monitor.Name] = monitor
|
|
}
|
|
}
|
|
}
|
|
monitors := make([]Monitor, 0, len(order))
|
|
for _, group := range order {
|
|
monitor, exists := byName[group]
|
|
if !exists {
|
|
monitor = Monitor{Name: group, Status: -1}
|
|
}
|
|
monitor.Group = group
|
|
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])
|
|
monitors = append(monitors, monitor)
|
|
}
|
|
return []UptimeGroupResult{{CategoryName: "Groups", Monitors: monitors}}, nil
|
|
}
|
|
|
|
func GetUptimeKumaStatus(c *gin.Context) {
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout)
|
|
defer cancel()
|
|
|
|
results, err := currentGroupMonitors(ctx, nil)
|
|
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 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
|
|
}
|
|
}
|