232 lines
7.4 KiB
Go
232 lines
7.4 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/fonu/fonu/internal/acme"
|
|
certstore "github.com/fonu/fonu/internal/certificate"
|
|
"github.com/fonu/fonu/internal/cloudflared"
|
|
"github.com/fonu/fonu/internal/config"
|
|
"github.com/fonu/fonu/internal/ddns"
|
|
"github.com/fonu/fonu/internal/logstore"
|
|
"github.com/fonu/fonu/internal/publicip"
|
|
"github.com/fonu/fonu/internal/service"
|
|
"github.com/fonu/fonu/internal/sysinfo"
|
|
)
|
|
|
|
type StatusHandler struct {
|
|
cfg config.Config
|
|
proxySvc *service.ProxyService
|
|
ddnsSvc *ddns.Service
|
|
acmeSvc *acme.Service
|
|
cloudflareSvc *cloudflared.Service
|
|
startedAt string
|
|
nginxPIDFile string
|
|
cpuMu sync.Mutex
|
|
cpuSample sysinfo.CPUSample
|
|
}
|
|
|
|
func NewStatusHandler(cfg config.Config, proxySvc *service.ProxyService, ddnsSvc *ddns.Service, acmeSvc *acme.Service, cloudflareSvc *cloudflared.Service, startedAt, nginxPIDFile string) *StatusHandler {
|
|
// 先采一次 CPU 计数器,使首次请求就能算出占用率(否则只能返回 0)
|
|
_, sample := sysinfo.Read(sysinfo.CPUSample{})
|
|
return &StatusHandler{
|
|
cfg: cfg,
|
|
proxySvc: proxySvc,
|
|
ddnsSvc: ddnsSvc,
|
|
acmeSvc: acmeSvc,
|
|
cloudflareSvc: cloudflareSvc,
|
|
startedAt: startedAt,
|
|
nginxPIDFile: nginxPIDFile,
|
|
cpuSample: sample,
|
|
}
|
|
}
|
|
|
|
type statusResponse struct {
|
|
PublicIPv4 string `json:"public_ipv4"`
|
|
PublicIPv6 string `json:"public_ipv6"`
|
|
PublicIPSource string `json:"public_ip_source"`
|
|
DDNSStatus string `json:"ddns_status"`
|
|
DDNSCount int `json:"ddns_count"`
|
|
DDNSLastUpdated string `json:"ddns_last_updated,omitempty"`
|
|
CertificateStatus string `json:"certificate_status"`
|
|
CertificateDays int `json:"certificate_days"`
|
|
CertificateCount int `json:"certificate_count"`
|
|
CertificateSummary string `json:"certificate_summary,omitempty"`
|
|
ProxyCount int `json:"proxy_count"`
|
|
NginxStatus string `json:"nginx_status"`
|
|
RequestToday int `json:"request_today"`
|
|
RequestYesterday int `json:"request_yesterday"`
|
|
RequestTrend *int `json:"request_trend,omitempty"`
|
|
RequestsHourly []int `json:"requests_hourly"`
|
|
RequestsHourlyLabels []string `json:"requests_hourly_labels"`
|
|
ErrorToday int `json:"error_today"`
|
|
AvgResponseMs float64 `json:"avg_response_ms"`
|
|
CPUPercent float64 `json:"cpu_percent"`
|
|
MemUsedBytes uint64 `json:"mem_used_bytes"`
|
|
MemTotalBytes uint64 `json:"mem_total_bytes"`
|
|
DiskUsedBytes uint64 `json:"disk_used_bytes"`
|
|
DiskTotalBytes uint64 `json:"disk_total_bytes"`
|
|
CloudflareCount int `json:"cloudflare_count"`
|
|
CloudflareOnline int `json:"cloudflare_online"`
|
|
CloudflareStatus string `json:"cloudflare_status"`
|
|
CloudflareMessage string `json:"cloudflare_message,omitempty"`
|
|
StartedAt string `json:"started_at"`
|
|
UptimeSeconds int64 `json:"uptime_seconds"`
|
|
}
|
|
|
|
func (h *StatusHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|
rules, err := h.proxySvc.List(r.Context())
|
|
if err != nil {
|
|
writeError(r, w, http.StatusInternalServerError, "读取状态失败")
|
|
return
|
|
}
|
|
|
|
nginxStatus := "unknown"
|
|
if _, err := os.Stat(h.nginxPIDFile); err == nil {
|
|
nginxStatus = "running"
|
|
} else if os.IsNotExist(err) {
|
|
nginxStatus = "stopped"
|
|
}
|
|
|
|
ddnsStatus, ddnsLastUpdated, ddnsCount := h.ddnsSvc.Summary(r.Context())
|
|
ipv4, ipv6, ipSource := h.resolvePublicIPs(r.Context())
|
|
|
|
records, _ := h.acmeSvc.List(r.Context())
|
|
certStatus, certDays, certCount, certSummary := summarizeCertificates(records)
|
|
|
|
resources, cpuSample := sysinfo.Read(h.currentCPUSample())
|
|
h.setCPUSample(cpuSample)
|
|
cfSummary := cloudflared.TunnelSummary{Status: "none", Message: "未创建隧道"}
|
|
if h.cloudflareSvc != nil {
|
|
cfSummary = h.cloudflareSvc.Summary(r.Context())
|
|
}
|
|
|
|
accessLog := filepath.Join(h.cfg.LogsDir(), "access.log")
|
|
total, errors, avgMs := logstore.CountTodayAccess(accessLog)
|
|
yesterday, _, _ := logstore.CountAccessForDay(accessLog, time.Now().AddDate(0, 0, -1))
|
|
hourly, hourlyLabels := logstore.HourlyAccessCounts(accessLog)
|
|
|
|
started, _ := time.Parse(time.RFC3339, h.startedAt)
|
|
uptime := time.Since(started).Seconds()
|
|
|
|
writeJSON(w, http.StatusOK, statusResponse{
|
|
PublicIPv4: ipv4,
|
|
PublicIPv6: ipv6,
|
|
PublicIPSource: ipSource,
|
|
DDNSStatus: ddnsStatus,
|
|
DDNSCount: ddnsCount,
|
|
DDNSLastUpdated: ddnsLastUpdated,
|
|
CertificateStatus: certStatus,
|
|
CertificateDays: certDays,
|
|
CertificateCount: certCount,
|
|
CertificateSummary: certSummary,
|
|
ProxyCount: len(rules),
|
|
NginxStatus: nginxStatus,
|
|
RequestToday: total,
|
|
RequestYesterday: yesterday,
|
|
RequestTrend: requestTrendPercent(total, yesterday),
|
|
RequestsHourly: hourly,
|
|
RequestsHourlyLabels: hourlyLabels,
|
|
ErrorToday: errors,
|
|
AvgResponseMs: avgMs,
|
|
CPUPercent: resources.CPUPercent,
|
|
MemUsedBytes: resources.MemUsedBytes,
|
|
MemTotalBytes: resources.MemTotalBytes,
|
|
DiskUsedBytes: resources.DiskUsedBytes,
|
|
DiskTotalBytes: resources.DiskTotalBytes,
|
|
CloudflareCount: cfSummary.Count,
|
|
CloudflareOnline: cfSummary.Online,
|
|
CloudflareStatus: cfSummary.Status,
|
|
CloudflareMessage: cfSummary.Message,
|
|
StartedAt: h.startedAt,
|
|
UptimeSeconds: int64(uptime),
|
|
})
|
|
}
|
|
|
|
// currentCPUSample 与 setCPUSample 用锁保护上次采样,/api/status 可能被并发调用
|
|
func (h *StatusHandler) currentCPUSample() sysinfo.CPUSample {
|
|
h.cpuMu.Lock()
|
|
defer h.cpuMu.Unlock()
|
|
return h.cpuSample
|
|
}
|
|
|
|
func (h *StatusHandler) setCPUSample(sample sysinfo.CPUSample) {
|
|
h.cpuMu.Lock()
|
|
defer h.cpuMu.Unlock()
|
|
h.cpuSample = sample
|
|
}
|
|
|
|
func (h *StatusHandler) resolvePublicIPs(ctx context.Context) (ipv4, ipv6, source string) {
|
|
if ipv4, ipv6, ok := h.ddnsSvc.PublicIPs(ctx); ok {
|
|
return ipv4, ipv6, "ddns"
|
|
}
|
|
if ipv4, ipv6, ok := h.ddnsSvc.LastKnownPublicIPs(ctx); ok {
|
|
return ipv4, ipv6, "ddns"
|
|
}
|
|
detectCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
|
defer cancel()
|
|
ipv4, ipv6, err := publicip.Detect(detectCtx)
|
|
if err != nil {
|
|
return "", ipv6, "none"
|
|
}
|
|
return ipv4, ipv6, "detect"
|
|
}
|
|
|
|
func summarizeCertificates(records []certstore.Record) (status string, days int, count int, summary string) {
|
|
if len(records) == 0 {
|
|
return "none", 0, 0, ""
|
|
}
|
|
count = len(records)
|
|
status = "ok"
|
|
days = records[0].DaysLeft
|
|
for _, rec := range records {
|
|
if rec.DaysLeft < days {
|
|
days = rec.DaysLeft
|
|
}
|
|
switch {
|
|
case rec.Status == "error" || rec.DaysLeft <= 0:
|
|
status = "error"
|
|
case rec.DaysLeft <= 30 && status != "error":
|
|
status = "warning"
|
|
}
|
|
}
|
|
if count == 1 {
|
|
summary = certificateSummaryName(records[0])
|
|
} else {
|
|
summary = fmt.Sprintf("%d 张证书", count)
|
|
}
|
|
return status, days, count, summary
|
|
}
|
|
|
|
func requestTrendPercent(today, yesterday int) *int {
|
|
if yesterday <= 0 {
|
|
if today <= 0 {
|
|
return nil
|
|
}
|
|
v := 100
|
|
return &v
|
|
}
|
|
v := int(math.Round(float64(today-yesterday) / float64(yesterday) * 100))
|
|
return &v
|
|
}
|
|
|
|
func certificateSummaryName(rec certstore.Record) string {
|
|
for _, domain := range rec.Domains {
|
|
if strings.HasPrefix(domain, "*.") {
|
|
return domain
|
|
}
|
|
}
|
|
if rec.Wildcard {
|
|
return "*." + rec.Domain
|
|
}
|
|
return rec.Domain
|
|
}
|