303 lines
6.8 KiB
Go
303 lines
6.8 KiB
Go
package services
|
||
|
||
import (
|
||
"log"
|
||
"sync"
|
||
"time"
|
||
|
||
"mengyaping-backend/config"
|
||
"mengyaping-backend/models"
|
||
"mengyaping-backend/storage"
|
||
"mengyaping-backend/utils"
|
||
)
|
||
|
||
// MonitorService 监控服务
|
||
type MonitorService struct {
|
||
httpClient *utils.HTTPClient
|
||
storage *storage.Storage
|
||
stopCh chan struct{}
|
||
running bool
|
||
mu sync.Mutex
|
||
}
|
||
|
||
var (
|
||
monitorService *MonitorService
|
||
monitorOnce sync.Once
|
||
)
|
||
|
||
// GetMonitorService 获取监控服务单例
|
||
func GetMonitorService() *MonitorService {
|
||
monitorOnce.Do(func() {
|
||
cfg := config.GetConfig()
|
||
monitorService = &MonitorService{
|
||
httpClient: utils.NewHTTPClient(cfg.Monitor.Timeout),
|
||
storage: storage.GetStorage(),
|
||
stopCh: make(chan struct{}),
|
||
}
|
||
})
|
||
return monitorService
|
||
}
|
||
|
||
// Start 启动监控服务
|
||
func (s *MonitorService) Start() {
|
||
s.mu.Lock()
|
||
if s.running {
|
||
s.mu.Unlock()
|
||
return
|
||
}
|
||
s.running = true
|
||
s.mu.Unlock()
|
||
|
||
log.Println("监控服务已启动")
|
||
|
||
// 立即执行一次检测
|
||
go s.checkAll()
|
||
|
||
// 定时检测
|
||
cfg := config.GetConfig()
|
||
ticker := time.NewTicker(cfg.Monitor.Interval)
|
||
defer ticker.Stop()
|
||
|
||
for {
|
||
select {
|
||
case <-ticker.C:
|
||
go s.checkAll()
|
||
case <-s.stopCh:
|
||
log.Println("监控服务已停止")
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// Stop 停止监控服务
|
||
func (s *MonitorService) Stop() {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
if s.running {
|
||
close(s.stopCh)
|
||
s.running = false
|
||
}
|
||
}
|
||
|
||
// checkAll 检查所有网站
|
||
func (s *MonitorService) checkAll() {
|
||
websites := s.storage.GetWebsites()
|
||
|
||
var wg sync.WaitGroup
|
||
semaphore := make(chan struct{}, 10) // 限制并发数
|
||
|
||
for _, website := range websites {
|
||
for _, urlInfo := range website.URLs {
|
||
wg.Add(1)
|
||
go func(w models.Website, u models.URLInfo) {
|
||
defer wg.Done()
|
||
semaphore <- struct{}{}
|
||
defer func() { <-semaphore }()
|
||
|
||
s.checkURL(w, u)
|
||
}(website, urlInfo)
|
||
}
|
||
}
|
||
|
||
wg.Wait()
|
||
|
||
// 保存记录
|
||
s.storage.SaveAll()
|
||
}
|
||
|
||
// checkURL 检查单个URL
|
||
func (s *MonitorService) checkURL(website models.Website, urlInfo models.URLInfo) {
|
||
result := s.httpClient.CheckWebsite(urlInfo.URL)
|
||
|
||
record := models.MonitorRecord{
|
||
WebsiteID: website.ID,
|
||
URLID: urlInfo.ID,
|
||
URL: urlInfo.URL,
|
||
StatusCode: result.StatusCode,
|
||
Latency: result.Latency.Milliseconds(),
|
||
IsUp: result.Error == nil && utils.IsSuccessStatus(result.StatusCode),
|
||
CheckedAt: time.Now(),
|
||
}
|
||
|
||
if result.Error != nil {
|
||
record.Error = result.Error.Error()
|
||
}
|
||
|
||
s.storage.AddRecord(record)
|
||
|
||
// 更新网站信息(标题和Favicon)
|
||
if result.Title != "" || result.Favicon != "" {
|
||
w := s.storage.GetWebsite(website.ID)
|
||
if w != nil {
|
||
needUpdate := false
|
||
if result.Title != "" && w.Title != result.Title {
|
||
w.Title = result.Title
|
||
needUpdate = true
|
||
}
|
||
if result.Favicon != "" && w.Favicon != result.Favicon {
|
||
w.Favicon = result.Favicon
|
||
needUpdate = true
|
||
}
|
||
if needUpdate {
|
||
w.UpdatedAt = time.Now()
|
||
s.storage.UpdateWebsite(*w)
|
||
}
|
||
}
|
||
}
|
||
|
||
log.Printf("检测 [%s] %s - 状态码: %d, 延迟: %dms, 可用: %v",
|
||
website.Name, urlInfo.URL, result.StatusCode, result.Latency.Milliseconds(), record.IsUp)
|
||
}
|
||
|
||
// CheckWebsiteNow 立即检查指定网站
|
||
func (s *MonitorService) CheckWebsiteNow(websiteID string) {
|
||
website := s.storage.GetWebsite(websiteID)
|
||
if website == nil {
|
||
return
|
||
}
|
||
|
||
for _, urlInfo := range website.URLs {
|
||
go s.checkURL(*website, urlInfo)
|
||
}
|
||
}
|
||
|
||
// GetWebsiteStatus 获取网站状态
|
||
func (s *MonitorService) GetWebsiteStatus(websiteID string) *models.WebsiteStatus {
|
||
website := s.storage.GetWebsite(websiteID)
|
||
if website == nil {
|
||
return nil
|
||
}
|
||
|
||
status := &models.WebsiteStatus{
|
||
Website: *website,
|
||
URLStatuses: []models.URLStatus{},
|
||
}
|
||
|
||
now := time.Now()
|
||
since24h := now.Add(-24 * time.Hour)
|
||
since7d := now.Add(-7 * 24 * time.Hour)
|
||
|
||
var totalUptime24h, totalUptime7d float64
|
||
var urlCount int
|
||
|
||
for _, urlInfo := range website.URLs {
|
||
urlStatus := s.getURLStatus(website.ID, urlInfo, since24h, since7d)
|
||
status.URLStatuses = append(status.URLStatuses, urlStatus)
|
||
|
||
totalUptime24h += urlStatus.Uptime24h
|
||
totalUptime7d += urlStatus.Uptime7d
|
||
urlCount++
|
||
}
|
||
|
||
if urlCount > 0 {
|
||
status.Uptime24h = totalUptime24h / float64(urlCount)
|
||
status.Uptime7d = totalUptime7d / float64(urlCount)
|
||
}
|
||
|
||
// 获取最后检测时间
|
||
for _, urlStatus := range status.URLStatuses {
|
||
if urlStatus.CurrentState.CheckedAt.After(status.LastChecked) {
|
||
status.LastChecked = urlStatus.CurrentState.CheckedAt
|
||
}
|
||
}
|
||
|
||
return status
|
||
}
|
||
|
||
// getURLStatus 获取URL状态
|
||
func (s *MonitorService) getURLStatus(websiteID string, urlInfo models.URLInfo, since24h, since7d time.Time) models.URLStatus {
|
||
urlStatus := models.URLStatus{
|
||
URLInfo: urlInfo,
|
||
}
|
||
|
||
// 获取最新记录
|
||
latest := s.storage.GetLatestRecord(websiteID, urlInfo.ID)
|
||
if latest != nil {
|
||
urlStatus.CurrentState = *latest
|
||
}
|
||
|
||
// 获取24小时记录
|
||
records24h := s.storage.GetRecords(websiteID, urlInfo.ID, since24h)
|
||
urlStatus.History24h = records24h
|
||
|
||
// 计算24小时可用率
|
||
if len(records24h) > 0 {
|
||
upCount := 0
|
||
var totalLatency int64
|
||
for _, r := range records24h {
|
||
if r.IsUp {
|
||
upCount++
|
||
}
|
||
totalLatency += r.Latency
|
||
}
|
||
urlStatus.Uptime24h = float64(upCount) / float64(len(records24h)) * 100
|
||
urlStatus.AvgLatency = totalLatency / int64(len(records24h))
|
||
}
|
||
|
||
// 获取7天记录并按小时统计
|
||
records7d := s.storage.GetRecords(websiteID, urlInfo.ID, since7d)
|
||
urlStatus.History7d = s.aggregateByHour(records7d)
|
||
|
||
// 计算7天可用率
|
||
if len(records7d) > 0 {
|
||
upCount := 0
|
||
for _, r := range records7d {
|
||
if r.IsUp {
|
||
upCount++
|
||
}
|
||
}
|
||
urlStatus.Uptime7d = float64(upCount) / float64(len(records7d)) * 100
|
||
}
|
||
|
||
return urlStatus
|
||
}
|
||
|
||
// aggregateByHour 按小时聚合记录
|
||
func (s *MonitorService) aggregateByHour(records []models.MonitorRecord) []models.HourlyStats {
|
||
hourlyMap := make(map[string]*models.HourlyStats)
|
||
|
||
for _, r := range records {
|
||
hourKey := r.CheckedAt.Truncate(time.Hour).Format(time.RFC3339)
|
||
|
||
if _, exists := hourlyMap[hourKey]; !exists {
|
||
hourlyMap[hourKey] = &models.HourlyStats{
|
||
Hour: r.CheckedAt.Truncate(time.Hour),
|
||
}
|
||
}
|
||
|
||
stats := hourlyMap[hourKey]
|
||
stats.TotalCount++
|
||
if r.IsUp {
|
||
stats.UpCount++
|
||
}
|
||
stats.AvgLatency += r.Latency
|
||
}
|
||
|
||
var result []models.HourlyStats
|
||
for _, stats := range hourlyMap {
|
||
if stats.TotalCount > 0 {
|
||
stats.AvgLatency /= int64(stats.TotalCount)
|
||
stats.Uptime = float64(stats.UpCount) / float64(stats.TotalCount) * 100
|
||
}
|
||
result = append(result, *stats)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// GetAllWebsiteStatuses 获取所有网站状态
|
||
func (s *MonitorService) GetAllWebsiteStatuses() []models.WebsiteStatus {
|
||
websites := s.storage.GetWebsites()
|
||
var statuses []models.WebsiteStatus
|
||
|
||
for _, website := range websites {
|
||
status := s.GetWebsiteStatus(website.ID)
|
||
if status != nil {
|
||
statuses = append(statuses, *status)
|
||
}
|
||
}
|
||
|
||
return statuses
|
||
}
|