feat: publish v1.0.0-rc.36-custom.1
Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 9m20s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 55m7s
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
Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 9m20s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 55m7s
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
This commit is contained in:
commit
bb7968b612
2381 files changed
+551494
No files matched your search
@@ -0,0 +1,294 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user