mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-29 02:10:58 +08:00
feat: 网站统计优化
This commit is contained in:
@@ -4,19 +4,26 @@ import "time"
|
||||
|
||||
// WebsiteStat 网站统计(每站每天一行或每小时一行)
|
||||
type WebsiteStat struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Site string `gorm:"not null;uniqueIndex:idx_wstat_unique" json:"site"`
|
||||
Date string `gorm:"not null;uniqueIndex:idx_wstat_unique;index" json:"date"`
|
||||
Hour int `gorm:"not null;uniqueIndex:idx_wstat_unique;default:-1" json:"hour"` // -1=每日汇总, 0-23=小时
|
||||
PV uint64 `gorm:"not null;default:0" json:"pv"`
|
||||
UV uint64 `gorm:"not null;default:0" json:"uv"`
|
||||
IP uint64 `gorm:"not null;default:0" json:"ip"`
|
||||
Bandwidth uint64 `gorm:"not null;default:0" json:"bandwidth"`
|
||||
Requests uint64 `gorm:"not null;default:0" json:"requests"`
|
||||
Errors uint64 `gorm:"not null;default:0" json:"errors"`
|
||||
Spiders uint64 `gorm:"not null;default:0" json:"spiders"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Site string `gorm:"not null;uniqueIndex:idx_wstat_unique" json:"site"`
|
||||
Date string `gorm:"not null;uniqueIndex:idx_wstat_unique;index" json:"date"`
|
||||
Hour int `gorm:"not null;uniqueIndex:idx_wstat_unique;default:-1" json:"hour"` // -1=每日汇总, 0-23=小时
|
||||
PV uint64 `gorm:"not null;default:0" json:"pv"`
|
||||
UV uint64 `gorm:"not null;default:0" json:"uv"`
|
||||
IP uint64 `gorm:"not null;default:0" json:"ip"`
|
||||
Bandwidth uint64 `gorm:"not null;default:0" json:"bandwidth"`
|
||||
BandwidthIn uint64 `gorm:"not null;default:0" json:"bandwidth_in"`
|
||||
Requests uint64 `gorm:"not null;default:0" json:"requests"`
|
||||
Errors uint64 `gorm:"not null;default:0" json:"errors"`
|
||||
Spiders uint64 `gorm:"not null;default:0" json:"spiders"`
|
||||
RequestTimeSum uint64 `gorm:"not null;default:0" json:"request_time_sum"`
|
||||
RequestTimeCount uint64 `gorm:"not null;default:0" json:"request_time_count"`
|
||||
Status2xx uint64 `gorm:"not null;default:0" json:"status_2xx"`
|
||||
Status3xx uint64 `gorm:"not null;default:0" json:"status_3xx"`
|
||||
Status4xx uint64 `gorm:"not null;default:0" json:"status_4xx"`
|
||||
Status5xx uint64 `gorm:"not null;default:0" json:"status_5xx"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// WebsiteErrorLog 网站错误日志(400-599 状态码详情)
|
||||
@@ -86,14 +93,21 @@ type WebsiteStatURI struct {
|
||||
|
||||
// WebsiteStatSeries 时间序列数据点(用于 API 返回)
|
||||
type WebsiteStatSeries struct {
|
||||
Key string `json:"key"` // 小时 "0"-"23" 或日期 "2026-02-18"
|
||||
PV uint64 `json:"pv"`
|
||||
UV uint64 `json:"uv"`
|
||||
IP uint64 `json:"ip"`
|
||||
Bandwidth uint64 `json:"bandwidth"`
|
||||
Requests uint64 `json:"requests"`
|
||||
Errors uint64 `json:"errors"`
|
||||
Spiders uint64 `json:"spiders"`
|
||||
Key string `json:"key"` // 小时 "0"-"23" 或日期 "2026-02-18"
|
||||
PV uint64 `json:"pv"`
|
||||
UV uint64 `json:"uv"`
|
||||
IP uint64 `json:"ip"`
|
||||
Bandwidth uint64 `json:"bandwidth"`
|
||||
BandwidthIn uint64 `json:"bandwidth_in"`
|
||||
Requests uint64 `json:"requests"`
|
||||
Errors uint64 `json:"errors"`
|
||||
Spiders uint64 `json:"spiders"`
|
||||
RequestTimeSum uint64 `json:"request_time_sum"`
|
||||
RequestTimeCount uint64 `json:"request_time_count"`
|
||||
Status2xx uint64 `json:"status_2xx"`
|
||||
Status3xx uint64 `json:"status_3xx"`
|
||||
Status4xx uint64 `json:"status_4xx"`
|
||||
Status5xx uint64 `json:"status_5xx"`
|
||||
}
|
||||
|
||||
// WebsiteStatSpiderRank 蜘蛛排名
|
||||
@@ -140,14 +154,21 @@ type WebsiteStatURIRank struct {
|
||||
|
||||
// WebsiteStatSiteItem 网站维度汇总
|
||||
type WebsiteStatSiteItem struct {
|
||||
Site string `json:"site"`
|
||||
PV uint64 `json:"pv"`
|
||||
UV uint64 `json:"uv"`
|
||||
IP uint64 `json:"ip"`
|
||||
Bandwidth uint64 `json:"bandwidth"`
|
||||
Requests uint64 `json:"requests"`
|
||||
Errors uint64 `json:"errors"`
|
||||
Spiders uint64 `json:"spiders"`
|
||||
Site string `json:"site"`
|
||||
PV uint64 `json:"pv"`
|
||||
UV uint64 `json:"uv"`
|
||||
IP uint64 `json:"ip"`
|
||||
Bandwidth uint64 `json:"bandwidth"`
|
||||
BandwidthIn uint64 `json:"bandwidth_in"`
|
||||
Requests uint64 `json:"requests"`
|
||||
Errors uint64 `json:"errors"`
|
||||
Spiders uint64 `json:"spiders"`
|
||||
RequestTimeSum uint64 `json:"request_time_sum"`
|
||||
RequestTimeCount uint64 `json:"request_time_count"`
|
||||
Status2xx uint64 `json:"status_2xx"`
|
||||
Status3xx uint64 `json:"status_3xx"`
|
||||
Status4xx uint64 `json:"status_4xx"`
|
||||
Status5xx uint64 `json:"status_5xx"`
|
||||
}
|
||||
|
||||
// WebsiteStatRepo 网站统计数据访问接口
|
||||
|
||||
@@ -26,14 +26,21 @@ func (r *websiteStatRepo) Upsert(stats []*biz.WebsiteStat) error {
|
||||
return batchUpsert(r.db, stats, clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "site"}, {Name: "date"}, {Name: "hour"}},
|
||||
DoUpdates: clause.Assignments(map[string]any{
|
||||
"pv": gorm.Expr("website_stats.pv + excluded.pv"),
|
||||
"uv": gorm.Expr("website_stats.uv + excluded.uv"),
|
||||
"ip": gorm.Expr("website_stats.ip + excluded.ip"),
|
||||
"bandwidth": gorm.Expr("website_stats.bandwidth + excluded.bandwidth"),
|
||||
"requests": gorm.Expr("website_stats.requests + excluded.requests"),
|
||||
"errors": gorm.Expr("website_stats.errors + excluded.errors"),
|
||||
"spiders": gorm.Expr("website_stats.spiders + excluded.spiders"),
|
||||
"updated_at": gorm.Expr("excluded.updated_at"),
|
||||
"pv": gorm.Expr("website_stats.pv + excluded.pv"),
|
||||
"uv": gorm.Expr("website_stats.uv + excluded.uv"),
|
||||
"ip": gorm.Expr("website_stats.ip + excluded.ip"),
|
||||
"bandwidth": gorm.Expr("website_stats.bandwidth + excluded.bandwidth"),
|
||||
"bandwidth_in": gorm.Expr("website_stats.bandwidth_in + excluded.bandwidth_in"),
|
||||
"requests": gorm.Expr("website_stats.requests + excluded.requests"),
|
||||
"errors": gorm.Expr("website_stats.errors + excluded.errors"),
|
||||
"spiders": gorm.Expr("website_stats.spiders + excluded.spiders"),
|
||||
"request_time_sum": gorm.Expr("website_stats.request_time_sum + excluded.request_time_sum"),
|
||||
"request_time_count": gorm.Expr("website_stats.request_time_count + excluded.request_time_count"),
|
||||
"status2xx": gorm.Expr("website_stats.status2xx + excluded.status2xx"),
|
||||
"status3xx": gorm.Expr("website_stats.status3xx + excluded.status3xx"),
|
||||
"status4xx": gorm.Expr("website_stats.status4xx + excluded.status4xx"),
|
||||
"status5xx": gorm.Expr("website_stats.status5xx + excluded.status5xx"),
|
||||
"updated_at": gorm.Expr("excluded.updated_at"),
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -41,7 +48,7 @@ func (r *websiteStatRepo) Upsert(stats []*biz.WebsiteStat) error {
|
||||
func (r *websiteStatRepo) ListByDateRange(start, end string, sites []string) ([]*biz.WebsiteStat, error) {
|
||||
var items []*biz.WebsiteStat
|
||||
q := r.db.Model(&biz.WebsiteStat{}).
|
||||
Select("site, SUM(pv) as pv, SUM(uv) as uv, SUM(ip) as ip, SUM(bandwidth) as bandwidth, SUM(requests) as requests, SUM(errors) as errors, SUM(spiders) as spiders").
|
||||
Select("site, SUM(pv) as pv, SUM(uv) as uv, SUM(ip) as ip, SUM(bandwidth) as bandwidth, SUM(bandwidth_in) as bandwidth_in, SUM(requests) as requests, SUM(errors) as errors, SUM(spiders) as spiders, SUM(request_time_sum) as request_time_sum, SUM(request_time_count) as request_time_count, SUM(status2xx) as status2xx, SUM(status3xx) as status3xx, SUM(status4xx) as status4xx, SUM(status5xx) as status5xx").
|
||||
Where("date BETWEEN ? AND ? AND hour = -1", start, end)
|
||||
if len(sites) > 0 {
|
||||
q = q.Where("site IN ?", sites)
|
||||
@@ -53,7 +60,7 @@ func (r *websiteStatRepo) ListByDateRange(start, end string, sites []string) ([]
|
||||
func (r *websiteStatRepo) DailySeries(start, end string, sites []string) ([]*biz.WebsiteStatSeries, error) {
|
||||
var series []*biz.WebsiteStatSeries
|
||||
q := r.db.Model(&biz.WebsiteStat{}).
|
||||
Select("date as key, COALESCE(SUM(pv), 0) as pv, COALESCE(SUM(uv), 0) as uv, COALESCE(SUM(ip), 0) as ip, COALESCE(SUM(bandwidth), 0) as bandwidth, COALESCE(SUM(requests), 0) as requests, COALESCE(SUM(errors), 0) as errors, COALESCE(SUM(spiders), 0) as spiders").
|
||||
Select("date as key, COALESCE(SUM(pv), 0) as pv, COALESCE(SUM(uv), 0) as uv, COALESCE(SUM(ip), 0) as ip, COALESCE(SUM(bandwidth), 0) as bandwidth, COALESCE(SUM(bandwidth_in), 0) as bandwidth_in, COALESCE(SUM(requests), 0) as requests, COALESCE(SUM(errors), 0) as errors, COALESCE(SUM(spiders), 0) as spiders, COALESCE(SUM(request_time_sum), 0) as request_time_sum, COALESCE(SUM(request_time_count), 0) as request_time_count, COALESCE(SUM(status2xx), 0) as status2xx, COALESCE(SUM(status3xx), 0) as status3xx, COALESCE(SUM(status4xx), 0) as status4xx, COALESCE(SUM(status5xx), 0) as status5xx").
|
||||
Where("date BETWEEN ? AND ? AND hour = -1", start, end)
|
||||
if len(sites) > 0 {
|
||||
q = q.Where("site IN ?", sites)
|
||||
@@ -65,7 +72,7 @@ func (r *websiteStatRepo) DailySeries(start, end string, sites []string) ([]*biz
|
||||
func (r *websiteStatRepo) HourlySeries(date string, sites []string) ([]*biz.WebsiteStatSeries, error) {
|
||||
var series []*biz.WebsiteStatSeries
|
||||
q := r.db.Model(&biz.WebsiteStat{}).
|
||||
Select("CAST(hour AS TEXT) as key, COALESCE(SUM(pv), 0) as pv, COALESCE(SUM(uv), 0) as uv, COALESCE(SUM(ip), 0) as ip, COALESCE(SUM(bandwidth), 0) as bandwidth, COALESCE(SUM(requests), 0) as requests, COALESCE(SUM(errors), 0) as errors, COALESCE(SUM(spiders), 0) as spiders").
|
||||
Select("CAST(hour AS TEXT) as key, COALESCE(SUM(pv), 0) as pv, COALESCE(SUM(uv), 0) as uv, COALESCE(SUM(ip), 0) as ip, COALESCE(SUM(bandwidth_in), 0) as bandwidth_in, COALESCE(SUM(bandwidth), 0) as bandwidth, COALESCE(SUM(requests), 0) as requests, COALESCE(SUM(errors), 0) as errors, COALESCE(SUM(spiders), 0) as spiders, COALESCE(SUM(request_time_sum), 0) as request_time_sum, COALESCE(SUM(request_time_count), 0) as request_time_count, COALESCE(SUM(status2xx), 0) as status2xx, COALESCE(SUM(status3xx), 0) as status3xx, COALESCE(SUM(status4xx), 0) as status4xx, COALESCE(SUM(status5xx), 0) as status5xx").
|
||||
Where("date = ? AND hour >= 0", date)
|
||||
if len(sites) > 0 {
|
||||
q = q.Where("site IN ?", sites)
|
||||
@@ -329,7 +336,7 @@ func (r *websiteStatRepo) ListErrors(start, end string, sites []string, status i
|
||||
func (r *websiteStatRepo) ListSiteStats(start, end string, sites []string) ([]*biz.WebsiteStatSiteItem, error) {
|
||||
var items []*biz.WebsiteStatSiteItem
|
||||
q := r.db.Model(&biz.WebsiteStat{}).
|
||||
Select("site, SUM(pv) as pv, SUM(uv) as uv, SUM(ip) as ip, SUM(bandwidth) as bandwidth, SUM(requests) as requests, SUM(errors) as errors, SUM(spiders) as spiders").
|
||||
Select("site, SUM(pv) as pv, SUM(uv) as uv, SUM(ip) as ip, SUM(bandwidth) as bandwidth, SUM(bandwidth_in) as bandwidth_in, SUM(requests) as requests, SUM(errors) as errors, SUM(spiders) as spiders, SUM(request_time_sum) as request_time_sum, SUM(request_time_count) as request_time_count, SUM(status2xx) as status2xx, SUM(status3xx) as status3xx, SUM(status4xx) as status4xx, SUM(status5xx) as status5xx").
|
||||
Where("date BETWEEN ? AND ? AND hour = -1", start, end)
|
||||
if len(sites) > 0 {
|
||||
q = q.Where("site IN ?", sites)
|
||||
|
||||
@@ -117,17 +117,24 @@ func (r *WebsiteStat) flush() {
|
||||
// 每日汇总行 (hour = -1)
|
||||
if siteHasData {
|
||||
stats = append(stats, &biz.WebsiteStat{
|
||||
Site: site,
|
||||
Date: date,
|
||||
Hour: -1,
|
||||
PV: snap.PV,
|
||||
UV: snap.UV,
|
||||
IP: snap.IP,
|
||||
Bandwidth: snap.Bandwidth,
|
||||
Requests: snap.Requests,
|
||||
Errors: snap.Errors,
|
||||
Spiders: snap.Spiders,
|
||||
UpdatedAt: now,
|
||||
Site: site,
|
||||
Date: date,
|
||||
Hour: -1,
|
||||
PV: snap.PV,
|
||||
UV: snap.UV,
|
||||
IP: snap.IP,
|
||||
Bandwidth: snap.Bandwidth,
|
||||
BandwidthIn: snap.BandwidthIn,
|
||||
Requests: snap.Requests,
|
||||
Errors: snap.Errors,
|
||||
Spiders: snap.Spiders,
|
||||
RequestTimeSum: snap.RequestTimeSum,
|
||||
RequestTimeCount: snap.RequestTimeCount,
|
||||
Status2xx: snap.Status2xx,
|
||||
Status3xx: snap.Status3xx,
|
||||
Status4xx: snap.Status4xx,
|
||||
Status5xx: snap.Status5xx,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -137,17 +144,24 @@ func (r *WebsiteStat) flush() {
|
||||
continue
|
||||
}
|
||||
stats = append(stats, &biz.WebsiteStat{
|
||||
Site: site,
|
||||
Date: date,
|
||||
Hour: h,
|
||||
PV: hs.PV,
|
||||
UV: hs.UV,
|
||||
IP: hs.IP,
|
||||
Bandwidth: hs.Bandwidth,
|
||||
Requests: hs.Requests,
|
||||
Errors: hs.Errors,
|
||||
Spiders: hs.Spiders,
|
||||
UpdatedAt: now,
|
||||
Site: site,
|
||||
Date: date,
|
||||
Hour: h,
|
||||
PV: hs.PV,
|
||||
UV: hs.UV,
|
||||
IP: hs.IP,
|
||||
Bandwidth: hs.Bandwidth,
|
||||
BandwidthIn: hs.BandwidthIn,
|
||||
Requests: hs.Requests,
|
||||
Errors: hs.Errors,
|
||||
Spiders: hs.Spiders,
|
||||
RequestTimeSum: hs.RequestTimeSum,
|
||||
RequestTimeCount: hs.RequestTimeCount,
|
||||
Status2xx: hs.Status2xx,
|
||||
Status3xx: hs.Status3xx,
|
||||
Status4xx: hs.Status4xx,
|
||||
Status5xx: hs.Status5xx,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -327,9 +341,16 @@ func isZeroSiteSnapshot(s *websitestat.SiteSnapshot) bool {
|
||||
s.UV == 0 &&
|
||||
s.IP == 0 &&
|
||||
s.Bandwidth == 0 &&
|
||||
s.BandwidthIn == 0 &&
|
||||
s.Requests == 0 &&
|
||||
s.Errors == 0 &&
|
||||
s.Spiders == 0
|
||||
s.Spiders == 0 &&
|
||||
s.RequestTimeSum == 0 &&
|
||||
s.RequestTimeCount == 0 &&
|
||||
s.Status2xx == 0 &&
|
||||
s.Status3xx == 0 &&
|
||||
s.Status4xx == 0 &&
|
||||
s.Status5xx == 0
|
||||
}
|
||||
|
||||
// isZeroHourSnapshot 判断小时快照是否全为零
|
||||
@@ -338,7 +359,14 @@ func isZeroHourSnapshot(h *websitestat.HourSnapshot) bool {
|
||||
h.UV == 0 &&
|
||||
h.IP == 0 &&
|
||||
h.Bandwidth == 0 &&
|
||||
h.BandwidthIn == 0 &&
|
||||
h.Requests == 0 &&
|
||||
h.Errors == 0 &&
|
||||
h.Spiders == 0
|
||||
h.Spiders == 0 &&
|
||||
h.RequestTimeSum == 0 &&
|
||||
h.RequestTimeCount == 0 &&
|
||||
h.Status2xx == 0 &&
|
||||
h.Status3xx == 0 &&
|
||||
h.Status4xx == 0 &&
|
||||
h.Status5xx == 0
|
||||
}
|
||||
|
||||
@@ -121,4 +121,13 @@ func init() {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
Migrations = append(Migrations, &gormigrate.Migration{
|
||||
ID: "20260221-add-perf-traffic-fields",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(&biz.WebsiteStat{})
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,14 +67,14 @@ func (s *WebsiteStatService) Overview(w http.ResponseWriter, r *http.Request) {
|
||||
// 查询当前周期汇总
|
||||
current, err := s.queryTotals(start, end, sites, today)
|
||||
if err != nil {
|
||||
ErrorSystem(w)
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 查询对比周期汇总
|
||||
previous, err := s.queryTotals(prevStart, prevEnd, sites, today)
|
||||
if err != nil {
|
||||
ErrorSystem(w)
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ func (s *WebsiteStatService) Overview(w http.ResponseWriter, r *http.Request) {
|
||||
series, err = s.queryDailySeries(start, end, sites, today)
|
||||
}
|
||||
if err != nil {
|
||||
ErrorSystem(w)
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -99,14 +99,14 @@ func (s *WebsiteStatService) Overview(w http.ResponseWriter, r *http.Request) {
|
||||
prevSeries, err = s.queryDailySeries(prevStart, prevEnd, sites, today)
|
||||
}
|
||||
if err != nil {
|
||||
ErrorSystem(w)
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有网站列表(用于站点选择器)
|
||||
websites, _, err := s.websiteRepo.List("all", 1, 10000)
|
||||
if err != nil {
|
||||
ErrorSystem(w)
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ func (s *WebsiteStatService) SiteStats(w http.ResponseWriter, r *http.Request) {
|
||||
// DB 查询全部日期范围(含今日)
|
||||
dbItems, err := s.statRepo.ListSiteStats(start, end, sites)
|
||||
if err != nil {
|
||||
ErrorSystem(w)
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
for _, item := range dbItems {
|
||||
@@ -166,19 +166,33 @@ func (s *WebsiteStatService) SiteStats(w http.ResponseWriter, r *http.Request) {
|
||||
existing.UV += snap.UV
|
||||
existing.IP += snap.IP
|
||||
existing.Bandwidth += snap.Bandwidth
|
||||
existing.BandwidthIn += snap.BandwidthIn
|
||||
existing.Requests += snap.Requests
|
||||
existing.Errors += snap.Errors
|
||||
existing.Spiders += snap.Spiders
|
||||
existing.RequestTimeSum += snap.RequestTimeSum
|
||||
existing.RequestTimeCount += snap.RequestTimeCount
|
||||
existing.Status2xx += snap.Status2xx
|
||||
existing.Status3xx += snap.Status3xx
|
||||
existing.Status4xx += snap.Status4xx
|
||||
existing.Status5xx += snap.Status5xx
|
||||
} else {
|
||||
siteMap[name] = &biz.WebsiteStatSiteItem{
|
||||
Site: name,
|
||||
PV: snap.PV,
|
||||
UV: snap.UV,
|
||||
IP: snap.IP,
|
||||
Bandwidth: snap.Bandwidth,
|
||||
Requests: snap.Requests,
|
||||
Errors: snap.Errors,
|
||||
Spiders: snap.Spiders,
|
||||
Site: name,
|
||||
PV: snap.PV,
|
||||
UV: snap.UV,
|
||||
IP: snap.IP,
|
||||
Bandwidth: snap.Bandwidth,
|
||||
BandwidthIn: snap.BandwidthIn,
|
||||
Requests: snap.Requests,
|
||||
Errors: snap.Errors,
|
||||
Spiders: snap.Spiders,
|
||||
RequestTimeSum: snap.RequestTimeSum,
|
||||
RequestTimeCount: snap.RequestTimeCount,
|
||||
Status2xx: snap.Status2xx,
|
||||
Status3xx: snap.Status3xx,
|
||||
Status4xx: snap.Status4xx,
|
||||
Status5xx: snap.Status5xx,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,7 +215,7 @@ func (s *WebsiteStatService) SpiderStats(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
items, err := s.statRepo.TopSpiders(start, end, sites, 50)
|
||||
if err != nil {
|
||||
ErrorSystem(w)
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -228,7 +242,7 @@ func (s *WebsiteStatService) ClientStats(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
items, err := s.statRepo.TopClients(start, end, sites, 100)
|
||||
if err != nil {
|
||||
ErrorSystem(w)
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -279,7 +293,7 @@ func (s *WebsiteStatService) IPStats(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
items, total, err := s.statRepo.TopIPs(start, end, sites, uint(page), uint(limit))
|
||||
if err != nil {
|
||||
ErrorSystem(w)
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -306,7 +320,7 @@ func (s *WebsiteStatService) GeoStats(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
items, err := s.statRepo.TopGeos(start, end, sites, groupBy, country, limit)
|
||||
if err != nil {
|
||||
ErrorSystem(w)
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -330,7 +344,7 @@ func (s *WebsiteStatService) URIStats(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
items, total, err := s.statRepo.TopURIs(start, end, sites, uint(page), uint(limit))
|
||||
if err != nil {
|
||||
ErrorSystem(w)
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -356,7 +370,7 @@ func (s *WebsiteStatService) ErrorStats(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
items, total, err := s.statRepo.ListErrors(start, end, sites, status, uint(page), uint(limit))
|
||||
if err != nil {
|
||||
ErrorSystem(w)
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -369,7 +383,7 @@ func (s *WebsiteStatService) ErrorStats(w http.ResponseWriter, r *http.Request)
|
||||
// Clear 清空所有统计数据
|
||||
func (s *WebsiteStatService) Clear(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.statRepo.Clear(); err != nil {
|
||||
ErrorSystem(w)
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
s.aggregator.Reset()
|
||||
@@ -424,13 +438,20 @@ func (s *WebsiteStatService) UpdateSetting(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
type statTotals struct {
|
||||
PV uint64 `json:"pv"`
|
||||
UV uint64 `json:"uv"`
|
||||
IP uint64 `json:"ip"`
|
||||
Bandwidth uint64 `json:"bandwidth"`
|
||||
Requests uint64 `json:"requests"`
|
||||
Errors uint64 `json:"errors"`
|
||||
Spiders uint64 `json:"spiders"`
|
||||
PV uint64 `json:"pv"`
|
||||
UV uint64 `json:"uv"`
|
||||
IP uint64 `json:"ip"`
|
||||
Bandwidth uint64 `json:"bandwidth"`
|
||||
BandwidthIn uint64 `json:"bandwidth_in"`
|
||||
Requests uint64 `json:"requests"`
|
||||
Errors uint64 `json:"errors"`
|
||||
Spiders uint64 `json:"spiders"`
|
||||
RequestTimeSum uint64 `json:"request_time_sum"`
|
||||
RequestTimeCount uint64 `json:"request_time_count"`
|
||||
Status2xx uint64 `json:"status_2xx"`
|
||||
Status3xx uint64 `json:"status_3xx"`
|
||||
Status4xx uint64 `json:"status_4xx"`
|
||||
Status5xx uint64 `json:"status_5xx"`
|
||||
}
|
||||
|
||||
// parseDateSites 解析公共查询参数 start, end, sites
|
||||
@@ -469,9 +490,16 @@ func (s *WebsiteStatService) queryTotals(start, end string, sites []string, toda
|
||||
total.UV += st.UV
|
||||
total.IP += st.IP
|
||||
total.Bandwidth += st.Bandwidth
|
||||
total.BandwidthIn += st.BandwidthIn
|
||||
total.Requests += st.Requests
|
||||
total.Errors += st.Errors
|
||||
total.Spiders += st.Spiders
|
||||
total.RequestTimeSum += st.RequestTimeSum
|
||||
total.RequestTimeCount += st.RequestTimeCount
|
||||
total.Status2xx += st.Status2xx
|
||||
total.Status3xx += st.Status3xx
|
||||
total.Status4xx += st.Status4xx
|
||||
total.Status5xx += st.Status5xx
|
||||
}
|
||||
|
||||
// 叠加今日未刷新的增量
|
||||
@@ -500,9 +528,16 @@ func (s *WebsiteStatService) mergeLiveTotals(total *statTotals, sites []string)
|
||||
total.UV += snap.UV
|
||||
total.IP += snap.IP
|
||||
total.Bandwidth += snap.Bandwidth
|
||||
total.BandwidthIn += snap.BandwidthIn
|
||||
total.Requests += snap.Requests
|
||||
total.Errors += snap.Errors
|
||||
total.Spiders += snap.Spiders
|
||||
total.RequestTimeSum += snap.RequestTimeSum
|
||||
total.RequestTimeCount += snap.RequestTimeCount
|
||||
total.Status2xx += snap.Status2xx
|
||||
total.Status3xx += snap.Status3xx
|
||||
total.Status4xx += snap.Status4xx
|
||||
total.Status5xx += snap.Status5xx
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,19 +578,33 @@ func (s *WebsiteStatService) queryHourlySeries(date string, sites []string, toda
|
||||
existing.UV += hs.UV
|
||||
existing.IP += hs.IP
|
||||
existing.Bandwidth += hs.Bandwidth
|
||||
existing.BandwidthIn += hs.BandwidthIn
|
||||
existing.Requests += hs.Requests
|
||||
existing.Errors += hs.Errors
|
||||
existing.Spiders += hs.Spiders
|
||||
existing.RequestTimeSum += hs.RequestTimeSum
|
||||
existing.RequestTimeCount += hs.RequestTimeCount
|
||||
existing.Status2xx += hs.Status2xx
|
||||
existing.Status3xx += hs.Status3xx
|
||||
existing.Status4xx += hs.Status4xx
|
||||
existing.Status5xx += hs.Status5xx
|
||||
} else {
|
||||
hourMap[h] = &biz.WebsiteStatSeries{
|
||||
Key: strconv.Itoa(h),
|
||||
PV: hs.PV,
|
||||
UV: hs.UV,
|
||||
IP: hs.IP,
|
||||
Bandwidth: hs.Bandwidth,
|
||||
Requests: hs.Requests,
|
||||
Errors: hs.Errors,
|
||||
Spiders: hs.Spiders,
|
||||
Key: strconv.Itoa(h),
|
||||
PV: hs.PV,
|
||||
UV: hs.UV,
|
||||
IP: hs.IP,
|
||||
Bandwidth: hs.Bandwidth,
|
||||
BandwidthIn: hs.BandwidthIn,
|
||||
Requests: hs.Requests,
|
||||
Errors: hs.Errors,
|
||||
Spiders: hs.Spiders,
|
||||
RequestTimeSum: hs.RequestTimeSum,
|
||||
RequestTimeCount: hs.RequestTimeCount,
|
||||
Status2xx: hs.Status2xx,
|
||||
Status3xx: hs.Status3xx,
|
||||
Status4xx: hs.Status4xx,
|
||||
Status5xx: hs.Status5xx,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -613,9 +662,16 @@ func (s *WebsiteStatService) queryDailySeries(start, end string, sites []string,
|
||||
todayData.UV += snap.UV
|
||||
todayData.IP += snap.IP
|
||||
todayData.Bandwidth += snap.Bandwidth
|
||||
todayData.BandwidthIn += snap.BandwidthIn
|
||||
todayData.Requests += snap.Requests
|
||||
todayData.Errors += snap.Errors
|
||||
todayData.Spiders += snap.Spiders
|
||||
todayData.RequestTimeSum += snap.RequestTimeSum
|
||||
todayData.RequestTimeCount += snap.RequestTimeCount
|
||||
todayData.Status2xx += snap.Status2xx
|
||||
todayData.Status3xx += snap.Status3xx
|
||||
todayData.Status4xx += snap.Status4xx
|
||||
todayData.Status5xx += snap.Status5xx
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,19 +28,23 @@ type Aggregator struct {
|
||||
}
|
||||
|
||||
type hourBucket struct {
|
||||
pv, requests, errors, spiders, bandwidth uint64
|
||||
ips map[string]struct{}
|
||||
uvs map[string]struct{}
|
||||
pv, requests, errors, spiders, bandwidth uint64
|
||||
bandwidthIn, requestTimeSum, requestTimeCount uint64
|
||||
status2xx, status3xx, status4xx, status5xx uint64
|
||||
ips map[string]struct{}
|
||||
uvs map[string]struct{}
|
||||
|
||||
// drain 后记录 set 大小,用于计算增量
|
||||
lastUVCount, lastIPCount uint64
|
||||
}
|
||||
|
||||
type siteDay struct {
|
||||
pv, requests, errors, spiders, bandwidth uint64
|
||||
ips map[string]struct{}
|
||||
uvs map[string]struct{}
|
||||
hours [24]*hourBucket
|
||||
pv, requests, errors, spiders, bandwidth uint64
|
||||
bandwidthIn, requestTimeSum, requestTimeCount uint64
|
||||
status2xx, status3xx, status4xx, status5xx uint64
|
||||
ips map[string]struct{}
|
||||
uvs map[string]struct{}
|
||||
hours [24]*hourBucket
|
||||
|
||||
// drain 后记录 set 大小,用于计算增量
|
||||
lastUVCount, lastIPCount uint64
|
||||
@@ -140,10 +144,37 @@ func (a *Aggregator) Record(entry *LogEntry) {
|
||||
// 更新每日总计
|
||||
sd.requests++
|
||||
sd.bandwidth += entry.Bytes
|
||||
sd.bandwidthIn += entry.ReqLength
|
||||
|
||||
// 更新小时桶
|
||||
hb.requests++
|
||||
hb.bandwidth += entry.Bytes
|
||||
hb.bandwidthIn += entry.ReqLength
|
||||
|
||||
// 请求耗时聚合
|
||||
if entry.RequestTime > 0 {
|
||||
ms := uint64(entry.RequestTime * 1000)
|
||||
sd.requestTimeSum += ms
|
||||
sd.requestTimeCount++
|
||||
hb.requestTimeSum += ms
|
||||
hb.requestTimeCount++
|
||||
}
|
||||
|
||||
// 状态码分组
|
||||
switch {
|
||||
case entry.Status >= 200 && entry.Status < 300:
|
||||
sd.status2xx++
|
||||
hb.status2xx++
|
||||
case entry.Status >= 300 && entry.Status < 400:
|
||||
sd.status3xx++
|
||||
hb.status3xx++
|
||||
case entry.Status >= 400 && entry.Status < 500:
|
||||
sd.status4xx++
|
||||
hb.status4xx++
|
||||
case entry.Status >= 500 && entry.Status < 600:
|
||||
sd.status5xx++
|
||||
hb.status5xx++
|
||||
}
|
||||
|
||||
// UV: IP+UA 去重(每日 + 每小时),超限后不再插入
|
||||
if len(sd.uvs) < a.UVMaxKeys {
|
||||
@@ -286,6 +317,13 @@ func (a *Aggregator) DrainSnapshot() (snapshotsByDate map[string]map[string]*Sit
|
||||
sd.errors = saturatingSub(sd.errors, snap.Errors)
|
||||
sd.spiders = saturatingSub(sd.spiders, snap.Spiders)
|
||||
sd.bandwidth = saturatingSub(sd.bandwidth, snap.Bandwidth)
|
||||
sd.bandwidthIn = saturatingSub(sd.bandwidthIn, snap.BandwidthIn)
|
||||
sd.requestTimeSum = saturatingSub(sd.requestTimeSum, snap.RequestTimeSum)
|
||||
sd.requestTimeCount = saturatingSub(sd.requestTimeCount, snap.RequestTimeCount)
|
||||
sd.status2xx = saturatingSub(sd.status2xx, snap.Status2xx)
|
||||
sd.status3xx = saturatingSub(sd.status3xx, snap.Status3xx)
|
||||
sd.status4xx = saturatingSub(sd.status4xx, snap.Status4xx)
|
||||
sd.status5xx = saturatingSub(sd.status5xx, snap.Status5xx)
|
||||
sd.lastUVCount = cappedAdd(sd.lastUVCount, snap.UV, uint64(len(sd.uvs)))
|
||||
sd.lastIPCount = cappedAdd(sd.lastIPCount, snap.IP, uint64(len(sd.ips)))
|
||||
|
||||
@@ -302,6 +340,13 @@ func (a *Aggregator) DrainSnapshot() (snapshotsByDate map[string]map[string]*Sit
|
||||
hb.errors = saturatingSub(hb.errors, hs.Errors)
|
||||
hb.spiders = saturatingSub(hb.spiders, hs.Spiders)
|
||||
hb.bandwidth = saturatingSub(hb.bandwidth, hs.Bandwidth)
|
||||
hb.bandwidthIn = saturatingSub(hb.bandwidthIn, hs.BandwidthIn)
|
||||
hb.requestTimeSum = saturatingSub(hb.requestTimeSum, hs.RequestTimeSum)
|
||||
hb.requestTimeCount = saturatingSub(hb.requestTimeCount, hs.RequestTimeCount)
|
||||
hb.status2xx = saturatingSub(hb.status2xx, hs.Status2xx)
|
||||
hb.status3xx = saturatingSub(hb.status3xx, hs.Status3xx)
|
||||
hb.status4xx = saturatingSub(hb.status4xx, hs.Status4xx)
|
||||
hb.status5xx = saturatingSub(hb.status5xx, hs.Status5xx)
|
||||
hb.lastUVCount = cappedAdd(hb.lastUVCount, hs.UV, uint64(len(hb.uvs)))
|
||||
hb.lastIPCount = cappedAdd(hb.lastIPCount, hs.IP, uint64(len(hb.ips)))
|
||||
}
|
||||
@@ -523,26 +568,40 @@ func snapshotHour(hb *hourBucket) *HourSnapshot {
|
||||
return nil
|
||||
}
|
||||
return &HourSnapshot{
|
||||
PV: hb.pv,
|
||||
UV: uint64(len(hb.uvs)) - hb.lastUVCount,
|
||||
IP: uint64(len(hb.ips)) - hb.lastIPCount,
|
||||
Bandwidth: hb.bandwidth,
|
||||
Requests: hb.requests,
|
||||
Errors: hb.errors,
|
||||
Spiders: hb.spiders,
|
||||
PV: hb.pv,
|
||||
UV: uint64(len(hb.uvs)) - hb.lastUVCount,
|
||||
IP: uint64(len(hb.ips)) - hb.lastIPCount,
|
||||
Bandwidth: hb.bandwidth,
|
||||
BandwidthIn: hb.bandwidthIn,
|
||||
Requests: hb.requests,
|
||||
Errors: hb.errors,
|
||||
Spiders: hb.spiders,
|
||||
RequestTimeSum: hb.requestTimeSum,
|
||||
RequestTimeCount: hb.requestTimeCount,
|
||||
Status2xx: hb.status2xx,
|
||||
Status3xx: hb.status3xx,
|
||||
Status4xx: hb.status4xx,
|
||||
Status5xx: hb.status5xx,
|
||||
}
|
||||
}
|
||||
|
||||
// snapshotSite 从 siteDay 生成增量快照
|
||||
func snapshotSite(sd *siteDay) *SiteSnapshot {
|
||||
snap := &SiteSnapshot{
|
||||
PV: sd.pv,
|
||||
UV: uint64(len(sd.uvs)) - sd.lastUVCount,
|
||||
IP: uint64(len(sd.ips)) - sd.lastIPCount,
|
||||
Bandwidth: sd.bandwidth,
|
||||
Requests: sd.requests,
|
||||
Errors: sd.errors,
|
||||
Spiders: sd.spiders,
|
||||
PV: sd.pv,
|
||||
UV: uint64(len(sd.uvs)) - sd.lastUVCount,
|
||||
IP: uint64(len(sd.ips)) - sd.lastIPCount,
|
||||
Bandwidth: sd.bandwidth,
|
||||
BandwidthIn: sd.bandwidthIn,
|
||||
Requests: sd.requests,
|
||||
Errors: sd.errors,
|
||||
Spiders: sd.spiders,
|
||||
RequestTimeSum: sd.requestTimeSum,
|
||||
RequestTimeCount: sd.requestTimeCount,
|
||||
Status2xx: sd.status2xx,
|
||||
Status3xx: sd.status3xx,
|
||||
Status4xx: sd.status4xx,
|
||||
Status5xx: sd.status5xx,
|
||||
}
|
||||
for h := range 24 {
|
||||
snap.Hours[h] = snapshotHour(sd.hours[h])
|
||||
@@ -552,14 +611,18 @@ func snapshotSite(sd *siteDay) *SiteSnapshot {
|
||||
|
||||
// hasPending 判断站点是否仍有未落库增量
|
||||
func hasPending(sd *siteDay) bool {
|
||||
if sd.pv > 0 || sd.requests > 0 || sd.errors > 0 || sd.spiders > 0 || sd.bandwidth > 0 {
|
||||
if sd.pv > 0 || sd.requests > 0 || sd.errors > 0 || sd.spiders > 0 || sd.bandwidth > 0 ||
|
||||
sd.bandwidthIn > 0 || sd.requestTimeSum > 0 || sd.requestTimeCount > 0 ||
|
||||
sd.status2xx > 0 || sd.status3xx > 0 || sd.status4xx > 0 || sd.status5xx > 0 {
|
||||
return true
|
||||
}
|
||||
for _, hb := range sd.hours {
|
||||
if hb == nil {
|
||||
continue
|
||||
}
|
||||
if hb.pv > 0 || hb.requests > 0 || hb.errors > 0 || hb.spiders > 0 || hb.bandwidth > 0 {
|
||||
if hb.pv > 0 || hb.requests > 0 || hb.errors > 0 || hb.spiders > 0 || hb.bandwidth > 0 ||
|
||||
hb.bandwidthIn > 0 || hb.requestTimeSum > 0 || hb.requestTimeCount > 0 ||
|
||||
hb.status2xx > 0 || hb.status3xx > 0 || hb.status4xx > 0 || hb.status5xx > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ func ParseLogEntry(tag string, data []byte) (*LogEntry, error) {
|
||||
Method: getString(v, "method"),
|
||||
ContentType: getString(v, "content_type"),
|
||||
ReqLength: uint64(getInt(v, "req_length")),
|
||||
RequestTime: getFloat(v, "rt"),
|
||||
}
|
||||
|
||||
// 仅 4xx/5xx 时才提取 body
|
||||
@@ -112,3 +113,20 @@ func getInt(v *fastjson.Value, key string) int64 {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// getFloat 从 JSON value 中提取浮点数字段
|
||||
func getFloat(v *fastjson.Value, key string) float64 {
|
||||
val := v.Get(key)
|
||||
if val == nil {
|
||||
return 0
|
||||
}
|
||||
switch val.Type() {
|
||||
case fastjson.TypeNumber:
|
||||
return val.GetFloat64()
|
||||
case fastjson.TypeString:
|
||||
f, _ := strconv.ParseFloat(string(val.GetStringBytes()), 64)
|
||||
return f
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
+40
-25
@@ -2,39 +2,54 @@ package websitestat
|
||||
|
||||
// LogEntry 解析后的访问日志条目
|
||||
type LogEntry struct {
|
||||
Site string // 来自 syslog tag
|
||||
URI string // 请求 URI
|
||||
Status int // HTTP 状态码
|
||||
Bytes uint64 // 响应体大小
|
||||
UA string // User-Agent
|
||||
IP string // 客户端 IP
|
||||
Method string // 请求方法
|
||||
Body string // 请求体(仅 400-599 状态码时保留)
|
||||
ContentType string // 响应 Content-Type(PV 判定用)
|
||||
ReqLength uint64 // 请求大小(入站流量)
|
||||
Site string // 来自 syslog tag
|
||||
URI string // 请求 URI
|
||||
Status int // HTTP 状态码
|
||||
Bytes uint64 // 响应体大小
|
||||
UA string // User-Agent
|
||||
IP string // 客户端 IP
|
||||
Method string // 请求方法
|
||||
Body string // 请求体(仅 400-599 状态码时保留)
|
||||
ContentType string // 响应 Content-Type(PV 判定用)
|
||||
ReqLength uint64 // 请求大小(入站流量)
|
||||
RequestTime float64 // 请求耗时(秒)
|
||||
}
|
||||
|
||||
// HourSnapshot 小时粒度快照
|
||||
type HourSnapshot struct {
|
||||
PV uint64 `json:"pv"`
|
||||
UV uint64 `json:"uv"`
|
||||
IP uint64 `json:"ip"`
|
||||
Bandwidth uint64 `json:"bandwidth"`
|
||||
Requests uint64 `json:"requests"`
|
||||
Errors uint64 `json:"errors"`
|
||||
Spiders uint64 `json:"spiders"`
|
||||
PV uint64 `json:"pv"`
|
||||
UV uint64 `json:"uv"`
|
||||
IP uint64 `json:"ip"`
|
||||
Bandwidth uint64 `json:"bandwidth"`
|
||||
BandwidthIn uint64 `json:"bandwidth_in"`
|
||||
Requests uint64 `json:"requests"`
|
||||
Errors uint64 `json:"errors"`
|
||||
Spiders uint64 `json:"spiders"`
|
||||
RequestTimeSum uint64 `json:"request_time_sum"`
|
||||
RequestTimeCount uint64 `json:"request_time_count"`
|
||||
Status2xx uint64 `json:"status_2xx"`
|
||||
Status3xx uint64 `json:"status_3xx"`
|
||||
Status4xx uint64 `json:"status_4xx"`
|
||||
Status5xx uint64 `json:"status_5xx"`
|
||||
}
|
||||
|
||||
// SiteSnapshot 站点快照(用于 DB flush)
|
||||
type SiteSnapshot struct {
|
||||
PV uint64 `json:"pv"`
|
||||
UV uint64 `json:"uv"`
|
||||
IP uint64 `json:"ip"`
|
||||
Bandwidth uint64 `json:"bandwidth"`
|
||||
Requests uint64 `json:"requests"`
|
||||
Errors uint64 `json:"errors"`
|
||||
Spiders uint64 `json:"spiders"`
|
||||
Hours [24]*HourSnapshot `json:"-"`
|
||||
PV uint64 `json:"pv"`
|
||||
UV uint64 `json:"uv"`
|
||||
IP uint64 `json:"ip"`
|
||||
Bandwidth uint64 `json:"bandwidth"`
|
||||
BandwidthIn uint64 `json:"bandwidth_in"`
|
||||
Requests uint64 `json:"requests"`
|
||||
Errors uint64 `json:"errors"`
|
||||
Spiders uint64 `json:"spiders"`
|
||||
RequestTimeSum uint64 `json:"request_time_sum"`
|
||||
RequestTimeCount uint64 `json:"request_time_count"`
|
||||
Status2xx uint64 `json:"status_2xx"`
|
||||
Status3xx uint64 `json:"status_3xx"`
|
||||
Status4xx uint64 `json:"status_4xx"`
|
||||
Status5xx uint64 `json:"status_5xx"`
|
||||
Hours [24]*HourSnapshot `json:"-"`
|
||||
}
|
||||
|
||||
// RealtimeStats 实时统计
|
||||
|
||||
@@ -26,6 +26,13 @@ interface StatTotals {
|
||||
requests: number
|
||||
errors: number
|
||||
spiders: number
|
||||
bandwidth_in: number
|
||||
request_time_sum: number
|
||||
request_time_count: number
|
||||
status_2xx: number
|
||||
status_3xx: number
|
||||
status_4xx: number
|
||||
status_5xx: number
|
||||
}
|
||||
|
||||
interface SeriesPoint {
|
||||
@@ -37,6 +44,13 @@ interface SeriesPoint {
|
||||
requests: number
|
||||
errors: number
|
||||
spiders: number
|
||||
bandwidth_in: number
|
||||
request_time_sum: number
|
||||
request_time_count: number
|
||||
status_2xx: number
|
||||
status_3xx: number
|
||||
status_4xx: number
|
||||
status_5xx: number
|
||||
}
|
||||
|
||||
interface OverviewData {
|
||||
@@ -54,7 +68,14 @@ const emptyTotals: StatTotals = {
|
||||
bandwidth: 0,
|
||||
requests: 0,
|
||||
errors: 0,
|
||||
spiders: 0
|
||||
spiders: 0,
|
||||
bandwidth_in: 0,
|
||||
request_time_sum: 0,
|
||||
request_time_count: 0,
|
||||
status_2xx: 0,
|
||||
status_3xx: 0,
|
||||
status_4xx: 0,
|
||||
status_5xx: 0
|
||||
}
|
||||
|
||||
const overview = ref<OverviewData>({
|
||||
@@ -239,20 +260,192 @@ const previousLabel = computed(() => {
|
||||
if (preset === 'yesterday') return $gettext('Day Before Yesterday')
|
||||
return $gettext('Previous Period')
|
||||
})
|
||||
|
||||
// ============ 性能/负载图表 ============
|
||||
|
||||
const perfChartOption = computed<EChartsOption>(() => {
|
||||
const series = overview.value.series || []
|
||||
const xData = series.map((s) => formatXLabel(s.key))
|
||||
const secondsPerSlot = isSingleDay.value ? 3600 : 86400
|
||||
|
||||
const qpsData = series.map((s) => {
|
||||
if (!s.requests) return 0
|
||||
return Number((s.requests / secondsPerSlot).toFixed(2))
|
||||
})
|
||||
const avgRtData = series.map((s) => {
|
||||
if (!s.request_time_count) return 0
|
||||
return Number((s.request_time_sum / s.request_time_count).toFixed(1))
|
||||
})
|
||||
|
||||
// 计算最新非零值用于 legend
|
||||
const lastQps = qpsData.findLast((v) => v > 0) ?? 0
|
||||
const lastRt = avgRtData.findLast((v) => v > 0) ?? 0
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: (params: any) => {
|
||||
if (!Array.isArray(params) || params.length === 0) return ''
|
||||
let html = `<div style="font-size:12px"><div style="margin-bottom:4px;font-weight:bold">${params[0].name}</div>`
|
||||
for (const p of params) {
|
||||
const unit = p.seriesIndex === 0 ? '' : 'ms'
|
||||
html += `<div>${p.marker} ${p.seriesName}: ${p.value}${unit}</div>`
|
||||
}
|
||||
html += '</div>'
|
||||
return html
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
formatter: (name: string) => {
|
||||
if (name === 'QPS') return `QPS: ${lastQps}`
|
||||
return `${$gettext('Avg Response Time')}: ${lastRt}ms`
|
||||
}
|
||||
},
|
||||
grid: { left: 50, right: 50, top: 40, bottom: 30 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: xData,
|
||||
axisLabel: {
|
||||
interval: isSingleDay.value
|
||||
? (_: number, value: string) => {
|
||||
const hour = parseInt(value.split(':')[0] || '0', 10)
|
||||
return hour % 3 === 0
|
||||
}
|
||||
: 'auto'
|
||||
}
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: 'QPS',
|
||||
splitLine: { lineStyle: { type: 'dashed' } }
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: 'ms',
|
||||
splitLine: { show: false }
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: 'QPS',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
areaStyle: { opacity: 0.15 },
|
||||
lineStyle: { width: 2 },
|
||||
itemStyle: { color: '#18a058' },
|
||||
data: qpsData
|
||||
},
|
||||
{
|
||||
name: $gettext('Avg Response Time'),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
yAxisIndex: 1,
|
||||
areaStyle: { opacity: 0.1 },
|
||||
lineStyle: { width: 2 },
|
||||
itemStyle: { color: '#2080f0' },
|
||||
data: avgRtData
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// ============ 流量图表 ============
|
||||
|
||||
const trafficChartOption = computed<EChartsOption>(() => {
|
||||
const series = overview.value.series || []
|
||||
const xData = series.map((s) => formatXLabel(s.key))
|
||||
|
||||
const outData = series.map((s) => s.bandwidth || 0)
|
||||
const inData = series.map((s) => s.bandwidth_in || 0)
|
||||
|
||||
// 汇总值用于 legend
|
||||
const totalOut = outData.reduce((a, b) => a + b, 0)
|
||||
const totalIn = inData.reduce((a, b) => a + b, 0)
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: (params: any) => {
|
||||
if (!Array.isArray(params) || params.length === 0) return ''
|
||||
let html = `<div style="font-size:12px"><div style="margin-bottom:4px;font-weight:bold">${params[0].name}</div>`
|
||||
for (const p of params) {
|
||||
html += `<div>${p.marker} ${p.seriesName}: ${formatBytes(p.value)}</div>`
|
||||
}
|
||||
html += '</div>'
|
||||
return html
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
formatter: (name: string) => {
|
||||
if (name === $gettext('Outbound')) return `${$gettext('Outbound')}: ${formatBytes(totalOut)}`
|
||||
return `${$gettext('Inbound')}: ${formatBytes(totalIn)}`
|
||||
}
|
||||
},
|
||||
grid: { left: 60, right: 20, top: 40, bottom: 30 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: xData,
|
||||
axisLabel: {
|
||||
interval: isSingleDay.value
|
||||
? (_: number, value: string) => {
|
||||
const hour = parseInt(value.split(':')[0] || '0', 10)
|
||||
return hour % 3 === 0
|
||||
}
|
||||
: 'auto'
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: { formatter: (v: number) => formatBytes(v) },
|
||||
splitLine: { lineStyle: { type: 'dashed' } }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: $gettext('Outbound'),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
areaStyle: { opacity: 0.15 },
|
||||
lineStyle: { width: 2 },
|
||||
itemStyle: { color: '#18a058' },
|
||||
data: outData
|
||||
},
|
||||
{
|
||||
name: $gettext('Inbound'),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
areaStyle: { opacity: 0.1 },
|
||||
lineStyle: { width: 2 },
|
||||
itemStyle: { color: '#2080f0' },
|
||||
data: inData
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex vertical :size="20">
|
||||
<!-- 统计卡片 -->
|
||||
<n-spin :show="loading">
|
||||
<div class="grid grid-cols-3 gap-12 sm:grid-cols-5 lg:grid-cols-9">
|
||||
<div class="gap-12 grid grid-cols-3 lg:grid-cols-9 sm:grid-cols-5">
|
||||
<n-card v-for="m in metrics" :key="m.key" :bordered="false" size="small">
|
||||
<div class="flex flex-col gap-4">
|
||||
<span class="text-12px text-[var(--text-color-3)]">{{ m.label }}</span>
|
||||
<span class="text-20px font-bold">{{
|
||||
formatValue(overview.current[m.key] || 0, m.isBytes)
|
||||
}}</span>
|
||||
<div class="text-12px flex items-center gap-4">
|
||||
<div class="text-12px flex gap-4 items-center">
|
||||
<span
|
||||
:class="{
|
||||
'text-[var(--success-color)]':
|
||||
@@ -294,7 +487,7 @@ const previousLabel = computed(() => {
|
||||
<!-- 趋势图 -->
|
||||
<n-card :bordered="false">
|
||||
<template #header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-8">
|
||||
<div class="flex flex-wrap gap-8 items-center justify-between">
|
||||
<n-radio-group v-model:value="activeMetric" size="small">
|
||||
<n-radio-button v-for="m in metrics" :key="m.key" :value="m.key">
|
||||
{{ m.label }}
|
||||
@@ -309,5 +502,23 @@ const previousLabel = computed(() => {
|
||||
<v-chart class="h-350px" :option="chartOption" autoresize />
|
||||
</n-spin>
|
||||
</n-card>
|
||||
|
||||
<!-- 性能/负载 + 流量 -->
|
||||
<n-grid :cols="2" :x-gap="20">
|
||||
<n-gi>
|
||||
<n-card :bordered="false" :title="$gettext('Performance / Load')">
|
||||
<n-spin :show="loading">
|
||||
<v-chart class="h-280px" :option="perfChartOption" autoresize />
|
||||
</n-spin>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card :bordered="false" :title="$gettext('Traffic')">
|
||||
<n-spin :show="loading">
|
||||
<v-chart class="h-280px" :option="trafficChartOption" autoresize />
|
||||
</n-spin>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-flex>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user