feat: 完善网站统计

This commit is contained in:
耗子
2026-02-18 19:50:33 +08:00
parent 7d368536d0
commit 2b48fd5fe3
22 changed files with 244 additions and 130 deletions
+1 -1
View File
@@ -7,7 +7,6 @@ import (
"regexp"
"strings"
"github.com/acepanel/panel/pkg/types"
"github.com/go-chi/chi/v5"
"github.com/leonelquinteros/gotext"
"github.com/spf13/cast"
@@ -20,6 +19,7 @@ import (
"github.com/acepanel/panel/pkg/shell"
"github.com/acepanel/panel/pkg/systemctl"
"github.com/acepanel/panel/pkg/tools"
"github.com/acepanel/panel/pkg/types"
)
type App struct {
+23 -23
View File
@@ -11,32 +11,32 @@ type SetRootPassword struct {
// ConfigTune MySQL 配置调整
type ConfigTune struct {
// 常规设置
Port string `form:"port" json:"port"`
MaxConnections string `form:"max_connections" json:"max_connections"`
MaxConnectErrors string `form:"max_connect_errors" json:"max_connect_errors"`
Port string `form:"port" json:"port"`
MaxConnections string `form:"max_connections" json:"max_connections"`
MaxConnectErrors string `form:"max_connect_errors" json:"max_connect_errors"`
DefaultStorageEngine string `form:"default_storage_engine" json:"default_storage_engine"`
TableOpenCache string `form:"table_open_cache" json:"table_open_cache"`
MaxAllowedPacket string `form:"max_allowed_packet" json:"max_allowed_packet"`
OpenFilesLimit string `form:"open_files_limit" json:"open_files_limit"`
TableOpenCache string `form:"table_open_cache" json:"table_open_cache"`
MaxAllowedPacket string `form:"max_allowed_packet" json:"max_allowed_packet"`
OpenFilesLimit string `form:"open_files_limit" json:"open_files_limit"`
// 性能调整
KeyBufferSize string `form:"key_buffer_size" json:"key_buffer_size"`
SortBufferSize string `form:"sort_buffer_size" json:"sort_buffer_size"`
ReadBufferSize string `form:"read_buffer_size" json:"read_buffer_size"`
ReadRndBufferSize string `form:"read_rnd_buffer_size" json:"read_rnd_buffer_size"`
JoinBufferSize string `form:"join_buffer_size" json:"join_buffer_size"`
ThreadCacheSize string `form:"thread_cache_size" json:"thread_cache_size"`
ThreadStack string `form:"thread_stack" json:"thread_stack"`
TmpTableSize string `form:"tmp_table_size" json:"tmp_table_size"`
MaxHeapTableSize string `form:"max_heap_table_size" json:"max_heap_table_size"`
MyisamSortBufferSize string `form:"myisam_sort_buffer_size" json:"myisam_sort_buffer_size"`
KeyBufferSize string `form:"key_buffer_size" json:"key_buffer_size"`
SortBufferSize string `form:"sort_buffer_size" json:"sort_buffer_size"`
ReadBufferSize string `form:"read_buffer_size" json:"read_buffer_size"`
ReadRndBufferSize string `form:"read_rnd_buffer_size" json:"read_rnd_buffer_size"`
JoinBufferSize string `form:"join_buffer_size" json:"join_buffer_size"`
ThreadCacheSize string `form:"thread_cache_size" json:"thread_cache_size"`
ThreadStack string `form:"thread_stack" json:"thread_stack"`
TmpTableSize string `form:"tmp_table_size" json:"tmp_table_size"`
MaxHeapTableSize string `form:"max_heap_table_size" json:"max_heap_table_size"`
MyisamSortBufferSize string `form:"myisam_sort_buffer_size" json:"myisam_sort_buffer_size"`
// InnoDB
InnodbBufferPoolSize string `form:"innodb_buffer_pool_size" json:"innodb_buffer_pool_size"`
InnodbLogBufferSize string `form:"innodb_log_buffer_size" json:"innodb_log_buffer_size"`
InnodbFlushLogAtTrxCommit string `form:"innodb_flush_log_at_trx_commit" json:"innodb_flush_log_at_trx_commit"`
InnodbLockWaitTimeout string `form:"innodb_lock_wait_timeout" json:"innodb_lock_wait_timeout"`
InnodbMaxDirtyPagesPct string `form:"innodb_max_dirty_pages_pct" json:"innodb_max_dirty_pages_pct"`
InnodbReadIoThreads string `form:"innodb_read_io_threads" json:"innodb_read_io_threads"`
InnodbWriteIoThreads string `form:"innodb_write_io_threads" json:"innodb_write_io_threads"`
InnodbBufferPoolSize string `form:"innodb_buffer_pool_size" json:"innodb_buffer_pool_size"`
InnodbLogBufferSize string `form:"innodb_log_buffer_size" json:"innodb_log_buffer_size"`
InnodbFlushLogAtTrxCommit string `form:"innodb_flush_log_at_trx_commit" json:"innodb_flush_log_at_trx_commit"`
InnodbLockWaitTimeout string `form:"innodb_lock_wait_timeout" json:"innodb_lock_wait_timeout"`
InnodbMaxDirtyPagesPct string `form:"innodb_max_dirty_pages_pct" json:"innodb_max_dirty_pages_pct"`
InnodbReadIoThreads string `form:"innodb_read_io_threads" json:"innodb_read_io_threads"`
InnodbWriteIoThreads string `form:"innodb_write_io_threads" json:"innodb_write_io_threads"`
// 日志
SlowQueryLog string `form:"slow_query_log" json:"slow_query_log"`
LongQueryTime string `form:"long_query_time" json:"long_query_time"`
+7 -7
View File
@@ -9,14 +9,14 @@ type UpdateConfig struct {
// ConfigTune Nginx 配置调整
type ConfigTune struct {
// 常规设置
WorkerProcesses string `form:"worker_processes" json:"worker_processes"`
WorkerConnections string `form:"worker_connections" json:"worker_connections"`
KeepaliveTimeout string `form:"keepalive_timeout" json:"keepalive_timeout"`
ClientMaxBodySize string `form:"client_max_body_size" json:"client_max_body_size"`
ClientBodyBufferSize string `form:"client_body_buffer_size" json:"client_body_buffer_size"`
ClientHeaderBufferSize string `form:"client_header_buffer_size" json:"client_header_buffer_size"`
WorkerProcesses string `form:"worker_processes" json:"worker_processes"`
WorkerConnections string `form:"worker_connections" json:"worker_connections"`
KeepaliveTimeout string `form:"keepalive_timeout" json:"keepalive_timeout"`
ClientMaxBodySize string `form:"client_max_body_size" json:"client_max_body_size"`
ClientBodyBufferSize string `form:"client_body_buffer_size" json:"client_body_buffer_size"`
ClientHeaderBufferSize string `form:"client_header_buffer_size" json:"client_header_buffer_size"`
ServerNamesHashBucketSize string `form:"server_names_hash_bucket_size" json:"server_names_hash_bucket_size"`
ServerTokens string `form:"server_tokens" json:"server_tokens"`
ServerTokens string `form:"server_tokens" json:"server_tokens"`
// Gzip 压缩
Gzip string `form:"gzip" json:"gzip"`
GzipMinLength string `form:"gzip_min_length" json:"gzip_min_length"`
+1 -1
View File
@@ -7,13 +7,13 @@ import (
"strings"
"time"
"github.com/acepanel/panel/pkg/db"
"github.com/go-chi/chi/v5"
"github.com/leonelquinteros/gotext"
"github.com/acepanel/panel/internal/app"
"github.com/acepanel/panel/internal/biz"
"github.com/acepanel/panel/internal/service"
"github.com/acepanel/panel/pkg/db"
"github.com/acepanel/panel/pkg/io"
"github.com/acepanel/panel/pkg/shell"
"github.com/acepanel/panel/pkg/systemctl"
+28 -24
View File
@@ -10,30 +10,34 @@ import (
type SettingKey string
const (
SettingKeyName SettingKey = "name"
SettingKeyVersion SettingKey = "version"
SettingKeyChannel SettingKey = "channel"
SettingKeyMonitor SettingKey = "monitor"
SettingKeyMonitorDays SettingKey = "monitor_days"
SettingKeyBackupPath SettingKey = "backup_path"
SettingKeyWebsitePath SettingKey = "website_path"
SettingKeyProjectPath SettingKey = "project_path"
SettingKeyContainerSock SettingKey = "container_sock"
SettingKeyWebsiteTLSVersions SettingKey = "website_tls_versions"
SettingKeyWebsiteCipherSuites SettingKey = "website_tls_cipher_suites"
SettingKeyMySQLRootPassword SettingKey = "mysql_root_password"
SettingKeyPostgresPassword SettingKey = "postgres_password"
SettingKeyOfflineMode SettingKey = "offline_mode"
SettingKeyAutoUpdate SettingKey = "auto_update"
SettingKeyWebserver SettingKey = "webserver"
SettingKeyPublicIPs SettingKey = "public_ips"
SettingHiddenMenu SettingKey = "hidden_menu"
SettingKeyCustomLogo SettingKey = "custom_logo"
SettingKeyMemo SettingKey = "memo"
SettingKeyScanAware SettingKey = "scan_aware"
SettingKeyScanAwareDays SettingKey = "scan_aware_days"
SettingKeyScanAwareInterfaces SettingKey = "scan_aware_interfaces"
SettingKeyWebsiteStatDays SettingKey = "website_stat_days"
SettingKeyName SettingKey = "name"
SettingKeyVersion SettingKey = "version"
SettingKeyChannel SettingKey = "channel"
SettingKeyMonitor SettingKey = "monitor"
SettingKeyMonitorDays SettingKey = "monitor_days"
SettingKeyBackupPath SettingKey = "backup_path"
SettingKeyWebsitePath SettingKey = "website_path"
SettingKeyProjectPath SettingKey = "project_path"
SettingKeyContainerSock SettingKey = "container_sock"
SettingKeyWebsiteTLSVersions SettingKey = "website_tls_versions"
SettingKeyWebsiteCipherSuites SettingKey = "website_tls_cipher_suites"
SettingKeyMySQLRootPassword SettingKey = "mysql_root_password"
SettingKeyPostgresPassword SettingKey = "postgres_password"
SettingKeyOfflineMode SettingKey = "offline_mode"
SettingKeyAutoUpdate SettingKey = "auto_update"
SettingKeyWebserver SettingKey = "webserver"
SettingKeyPublicIPs SettingKey = "public_ips"
SettingHiddenMenu SettingKey = "hidden_menu"
SettingKeyCustomLogo SettingKey = "custom_logo"
SettingKeyMemo SettingKey = "memo"
SettingKeyScanAware SettingKey = "scan_aware"
SettingKeyScanAwareDays SettingKey = "scan_aware_days"
SettingKeyScanAwareInterfaces SettingKey = "scan_aware_interfaces"
SettingKeyWebsiteStatDays SettingKey = "website_stat_days"
SettingKeyWebsiteStatErrBufMax SettingKey = "website_stat_err_buf_max"
SettingKeyWebsiteStatUVMaxKeys SettingKey = "website_stat_uv_max_keys"
SettingKeyWebsiteStatIPMaxKeys SettingKey = "website_stat_ip_max_keys"
SettingKeyWebsiteStatDetailMaxKeys SettingKey = "website_stat_detail_max_keys"
)
type Setting struct {
+26 -16
View File
@@ -24,7 +24,7 @@ func (r *websiteStatRepo) Upsert(stats []*biz.WebsiteStat) error {
return nil
}
return r.db.Clauses(clause.OnConflict{
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("excluded.pv"),
@@ -36,7 +36,7 @@ func (r *websiteStatRepo) Upsert(stats []*biz.WebsiteStat) error {
"spiders": gorm.Expr("excluded.spiders"),
"updated_at": gorm.Expr("excluded.updated_at"),
}),
}).Create(stats).Error
})
}
func (r *websiteStatRepo) ListByDateRange(start, end string, sites []string) ([]*biz.WebsiteStat, error) {
@@ -84,12 +84,8 @@ func (r *websiteStatRepo) InsertErrors(errors []*biz.WebsiteErrorLog) error {
return nil
}
const batchSize = 100
for i := 0; i < len(errors); i += batchSize {
end := i + batchSize
if end > len(errors) {
end = len(errors)
}
for i := 0; i < len(errors); i += upsertBatchSize {
end := min(i+upsertBatchSize, len(errors))
if err := r.db.Create(errors[i:end]).Error; err != nil {
return err
}
@@ -126,13 +122,13 @@ func (r *websiteStatRepo) UpsertSpiders(stats []*biz.WebsiteStatSpider) error {
if len(stats) == 0 {
return nil
}
return r.db.Clauses(clause.OnConflict{
return batchUpsert(r.db, stats, clause.OnConflict{
Columns: []clause.Column{{Name: "site"}, {Name: "date"}, {Name: "spider"}},
DoUpdates: clause.Assignments(map[string]any{
"requests": gorm.Expr("website_stat_spiders.requests + excluded.requests"),
"updated_at": gorm.Expr("excluded.updated_at"),
}),
}).Create(stats).Error
})
}
func (r *websiteStatRepo) TopSpiders(start, end string, sites []string, limit uint) ([]*biz.WebsiteStatSpiderRank, error) {
@@ -157,13 +153,13 @@ func (r *websiteStatRepo) UpsertClients(stats []*biz.WebsiteStatClient) error {
if len(stats) == 0 {
return nil
}
return r.db.Clauses(clause.OnConflict{
return batchUpsert(r.db, stats, clause.OnConflict{
Columns: []clause.Column{{Name: "site"}, {Name: "date"}, {Name: "browser"}, {Name: "os"}},
DoUpdates: clause.Assignments(map[string]any{
"requests": gorm.Expr("website_stat_clients.requests + excluded.requests"),
"updated_at": gorm.Expr("excluded.updated_at"),
}),
}).Create(stats).Error
})
}
func (r *websiteStatRepo) TopClients(start, end string, sites []string, limit uint) ([]*biz.WebsiteStatClientRank, error) {
@@ -188,14 +184,14 @@ func (r *websiteStatRepo) UpsertIPs(stats []*biz.WebsiteStatIP) error {
if len(stats) == 0 {
return nil
}
return r.db.Clauses(clause.OnConflict{
return batchUpsert(r.db, stats, clause.OnConflict{
Columns: []clause.Column{{Name: "site"}, {Name: "date"}, {Name: "ip"}},
DoUpdates: clause.Assignments(map[string]any{
"requests": gorm.Expr("website_stat_ips.requests + excluded.requests"),
"bandwidth": gorm.Expr("website_stat_ips.bandwidth + excluded.bandwidth"),
"updated_at": gorm.Expr("excluded.updated_at"),
}),
}).Create(stats).Error
})
}
func (r *websiteStatRepo) TopIPs(start, end string, sites []string, page, limit uint) ([]*biz.WebsiteStatIPRank, uint, error) {
@@ -236,7 +232,7 @@ func (r *websiteStatRepo) UpsertURIs(stats []*biz.WebsiteStatURI) error {
if len(stats) == 0 {
return nil
}
return r.db.Clauses(clause.OnConflict{
return batchUpsert(r.db, stats, clause.OnConflict{
Columns: []clause.Column{{Name: "site"}, {Name: "date"}, {Name: "uri"}},
DoUpdates: clause.Assignments(map[string]any{
"requests": gorm.Expr("website_stat_uris.requests + excluded.requests"),
@@ -244,7 +240,7 @@ func (r *websiteStatRepo) UpsertURIs(stats []*biz.WebsiteStatURI) error {
"errors": gorm.Expr("website_stat_uris.errors + excluded.errors"),
"updated_at": gorm.Expr("excluded.updated_at"),
}),
}).Create(stats).Error
})
}
func (r *websiteStatRepo) TopURIs(start, end string, sites []string, page, limit uint) ([]*biz.WebsiteStatURIRank, uint, error) {
@@ -314,3 +310,17 @@ func (r *websiteStatRepo) ListSiteStats(start, end string, sites []string) ([]*b
err := q.Group("site").Order("requests DESC").Scan(&items).Error
return items, err
}
// upsert 分批大小,避免超出 SQLite 变量数限制
const upsertBatchSize = 100
// batchUpsert 通用分批 upsert 辅助函数
func batchUpsert[T any](db *gorm.DB, items []T, conflict clause.OnConflict) error {
for i := 0; i < len(items); i += upsertBatchSize {
end := min(i+upsertBatchSize, len(items))
if err := db.Clauses(conflict).Create(items[i:end]).Error; err != nil {
return err
}
}
return nil
}
+5 -1
View File
@@ -2,5 +2,9 @@ package request
// WebsiteStatSetting 网站统计设置
type WebsiteStatSetting struct {
Days uint `json:"days" validate:"required|min:1|max:365"`
Days uint `json:"days" validate:"required|min:1|max:365"`
ErrBufMax int `json:"err_buf_max" validate:"min:0|max:1000000"`
UVMaxKeys int `json:"uv_max_keys" validate:"min:0|max:100000000"`
IPMaxKeys int `json:"ip_max_keys" validate:"min:0|max:100000000"`
DetailMaxKeys int `json:"detail_max_keys" validate:"min:0|max:100000000"`
}
+13
View File
@@ -48,6 +48,19 @@ func (r *WebsiteStat) ensureListener() {
return
}
if v, err := r.setting.GetInt(biz.SettingKeyWebsiteStatErrBufMax); err == nil && v > 0 {
r.aggregator.ErrBufMaxSize = v
}
if v, err := r.setting.GetInt(biz.SettingKeyWebsiteStatUVMaxKeys); err == nil && v > 0 {
r.aggregator.UVMaxKeys = v
}
if v, err := r.setting.GetInt(biz.SettingKeyWebsiteStatIPMaxKeys); err == nil && v > 0 {
r.aggregator.IPMaxKeys = v
}
if v, err := r.setting.GetInt(biz.SettingKeyWebsiteStatDetailMaxKeys); err == nil && v > 0 {
r.aggregator.DetailMaxKeys = v
}
listener, err := websitestat.NewListener("/tmp/ace_stats.sock", r.log)
if err != nil {
r.log.Warn("fail to start website stat listener", slog.Any("err", err))
+2 -2
View File
@@ -7,8 +7,8 @@ import (
)
type Ws struct {
ws *service.WsService
toolboxMigration *service.ToolboxMigrationService
ws *service.WsService
toolboxMigration *service.ToolboxMigrationService
}
func NewWs(ws *service.WsService, toolboxMigration *service.ToolboxMigrationService) *Ws {
+5
View File
@@ -1026,6 +1026,11 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
{Key: biz.SettingHiddenMenu, Value: "[]"},
{Key: biz.SettingKeyScanAware, Value: "true"},
{Key: biz.SettingKeyScanAwareDays, Value: "30"},
{Key: biz.SettingKeyWebsiteStatDays, Value: "30"},
{Key: biz.SettingKeyWebsiteStatErrBufMax, Value: "10000"},
{Key: biz.SettingKeyWebsiteStatUVMaxKeys, Value: "1000000"},
{Key: biz.SettingKeyWebsiteStatIPMaxKeys, Value: "500000"},
{Key: biz.SettingKeyWebsiteStatDetailMaxKeys, Value: "100000"},
}
if err = s.db.Create(&settings).Error; err != nil {
return errors.New(s.t.Get("Initialization failed: %v", err))
+21 -1
View File
@@ -360,8 +360,16 @@ func (s *WebsiteStatService) Clear(w http.ResponseWriter, r *http.Request) {
// GetSetting 获取统计设置
func (s *WebsiteStatService) GetSetting(w http.ResponseWriter, r *http.Request) {
days, _ := s.setting.GetInt(biz.SettingKeyWebsiteStatDays, 30)
errBufMax, _ := s.setting.GetInt(biz.SettingKeyWebsiteStatErrBufMax, 10000)
uvMaxKeys, _ := s.setting.GetInt(biz.SettingKeyWebsiteStatUVMaxKeys, 1000000)
ipMaxKeys, _ := s.setting.GetInt(biz.SettingKeyWebsiteStatIPMaxKeys, 500000)
detailMaxKeys, _ := s.setting.GetInt(biz.SettingKeyWebsiteStatDetailMaxKeys, 100000)
Success(w, chix.M{
"days": days,
"days": days,
"err_buf_max": errBufMax,
"uv_max_keys": uvMaxKeys,
"ip_max_keys": ipMaxKeys,
"detail_max_keys": detailMaxKeys,
})
}
@@ -377,6 +385,18 @@ func (s *WebsiteStatService) UpdateSetting(w http.ResponseWriter, r *http.Reques
Error(w, http.StatusInternalServerError, "%v", err)
return
}
if req.ErrBufMax > 0 {
_ = s.setting.Set(biz.SettingKeyWebsiteStatErrBufMax, fmt.Sprintf("%d", req.ErrBufMax))
}
if req.UVMaxKeys > 0 {
_ = s.setting.Set(biz.SettingKeyWebsiteStatUVMaxKeys, fmt.Sprintf("%d", req.UVMaxKeys))
}
if req.IPMaxKeys > 0 {
_ = s.setting.Set(biz.SettingKeyWebsiteStatIPMaxKeys, fmt.Sprintf("%d", req.IPMaxKeys))
}
if req.DetailMaxKeys > 0 {
_ = s.setting.Set(biz.SettingKeyWebsiteStatDetailMaxKeys, fmt.Sprintf("%d", req.DetailMaxKeys))
}
Success(w, nil)
}
+2 -1
View File
@@ -8,8 +8,9 @@ import (
"strconv"
"strings"
"github.com/acepanel/panel/pkg/webserver/types"
"github.com/samber/lo"
"github.com/acepanel/panel/pkg/webserver/types"
)
// parseDurationToSeconds 将时长字符串转换为秒数
+2 -1
View File
@@ -8,8 +8,9 @@ import (
"strconv"
"strings"
"github.com/acepanel/panel/pkg/webserver/types"
"github.com/samber/lo"
"github.com/acepanel/panel/pkg/webserver/types"
)
// redirectFilePattern 匹配重定向配置文件名 (100-199)
+2 -1
View File
@@ -6,8 +6,9 @@ import (
"path/filepath"
"strings"
"github.com/acepanel/panel/pkg/webserver/types"
"github.com/samber/lo"
"github.com/acepanel/panel/pkg/webserver/types"
)
// StaticVhost 纯静态虚拟主机
+2 -1
View File
@@ -8,8 +8,9 @@ import (
"strconv"
"strings"
"github.com/acepanel/panel/pkg/webserver/types"
"github.com/samber/lo"
"github.com/acepanel/panel/pkg/webserver/types"
)
// redirectFilePattern 匹配重定向配置文件名 (100-199)
+2 -1
View File
@@ -9,8 +9,9 @@ import (
"strings"
"time"
"github.com/acepanel/panel/pkg/webserver/types"
"github.com/samber/lo"
"github.com/acepanel/panel/pkg/webserver/types"
)
// upstreamFilePattern 匹配 upstream 配置文件名 (100-XXX-name.conf)
+46 -28
View File
@@ -6,9 +6,6 @@ import (
"time"
)
// 详细统计每站每天最大条目数
const detailMaxKeys = 100000
// Aggregator 内存聚合器,将日志条目聚合为站点统计快照
type Aggregator struct {
mu sync.Mutex
@@ -18,6 +15,12 @@ type Aggregator struct {
// 错误日志缓冲
errBuf []*ErrorEntry
// 可配置上限
ErrBufMaxSize int // 错误缓冲最大条目数,默认 10000
UVMaxKeys int // 每站每天 UV 去重 set 上限,默认 1000000
IPMaxKeys int // 每站每天 IP 去重 set 上限,默认 500000
DetailMaxKeys int // 详细统计(蜘蛛/客户端/IP/URI)每站每天最大条目数,默认 100000
// 实时统计(滑动窗口 60 秒)
rtSlots [60]rtSlot
rtSec int64
@@ -65,8 +68,12 @@ type rtSlot struct {
// NewAggregator 创建聚合器实例
func NewAggregator() *Aggregator {
return &Aggregator{
sites: make(map[string]*siteDay),
date: time.Now().Format(time.DateOnly),
sites: make(map[string]*siteDay),
date: time.Now().Format(time.DateOnly),
ErrBufMaxSize: 10000,
UVMaxKeys: 1000000,
IPMaxKeys: 500000,
DetailMaxKeys: 100000,
}
}
@@ -87,6 +94,17 @@ func (a *Aggregator) Record(entry *LogEntry) {
today := now.Format(time.DateOnly)
hour := now.Hour()
// 锁外预计算 CPU 密集操作
spiderName := SpiderName(entry.UA)
var browser, os string
if spiderName == "" {
browser, os = ParseUA(entry.UA)
}
uvKey := entry.IP + "|" + entry.UA
isErr := entry.Status >= 400 && entry.Status < 600
isPV := IsPageView(entry)
clientKey := browser + "|" + os
a.mu.Lock()
// 日期切换,重置所有数据
@@ -119,25 +137,25 @@ func (a *Aggregator) Record(entry *LogEntry) {
hb.requests++
hb.bandwidth += entry.Bytes
// UV: IP+UA 去重(每日 + 每小时)
uvKey := entry.IP + "|" + entry.UA
sd.uvs[uvKey] = struct{}{}
// UV: IP+UA 去重(每日 + 每小时),超限后不再插入
if len(sd.uvs) < a.UVMaxKeys {
sd.uvs[uvKey] = struct{}{}
}
hb.uvs[uvKey] = struct{}{}
// IP 去重(每日 + 每小时)
sd.ips[entry.IP] = struct{}{}
// IP 去重(每日 + 每小时),超限后不再插入
if len(sd.ips) < a.IPMaxKeys {
sd.ips[entry.IP] = struct{}{}
}
hb.ips[entry.IP] = struct{}{}
// PV 判定
if IsPageView(entry) {
if isPV {
sd.pv++
hb.pv++
}
isErr := entry.Status >= 400 && entry.Status < 600
// 蜘蛛检测
spiderName := SpiderName(entry.UA)
if spiderName != "" {
sd.spiders++
hb.spiders++
@@ -145,11 +163,9 @@ func (a *Aggregator) Record(entry *LogEntry) {
sd.spiderCounts[spiderName]++
} else {
// 非蜘蛛请求才统计客户端(浏览器/OS)
browser, os := ParseUA(entry.UA)
clientKey := browser + "|" + os
if cc, exists := sd.clientCounts[clientKey]; exists {
cc.requests++
} else if len(sd.clientCounts) < detailMaxKeys {
} else if len(sd.clientCounts) < a.DetailMaxKeys {
sd.clientCounts[clientKey] = &clientCount{requests: 1}
}
}
@@ -158,7 +174,7 @@ func (a *Aggregator) Record(entry *LogEntry) {
if ic, exists := sd.ipCounts[entry.IP]; exists {
ic.requests++
ic.bandwidth += entry.Bytes
} else if len(sd.ipCounts) < detailMaxKeys {
} else if len(sd.ipCounts) < a.DetailMaxKeys {
sd.ipCounts[entry.IP] = &ipCount{requests: 1, bandwidth: entry.Bytes}
}
@@ -169,7 +185,7 @@ func (a *Aggregator) Record(entry *LogEntry) {
if isErr {
uc.errors++
}
} else if len(sd.uriCounts) < detailMaxKeys {
} else if len(sd.uriCounts) < a.DetailMaxKeys {
uc := &uriCount{requests: 1, bandwidth: entry.Bytes}
if isErr {
uc.errors = 1
@@ -181,15 +197,17 @@ func (a *Aggregator) Record(entry *LogEntry) {
if isErr {
sd.errors++
hb.errors++
a.errBuf = append(a.errBuf, &ErrorEntry{
Site: entry.Site,
URI: entry.URI,
Method: entry.Method,
IP: entry.IP,
UA: entry.UA,
Body: entry.Body,
Status: entry.Status,
})
if len(a.errBuf) < a.ErrBufMaxSize {
a.errBuf = append(a.errBuf, &ErrorEntry{
Site: entry.Site,
URI: entry.URI,
Method: entry.Method,
IP: entry.IP,
UA: entry.UA,
Body: entry.Body,
Status: entry.Status,
})
}
}
a.mu.Unlock()
+4 -3
View File
@@ -12,6 +12,7 @@ type Listener struct {
path string
conn *net.UnixConn
log *slog.Logger
buf []byte // 读缓冲区,在 readLoop 单 goroutine 中复用
}
// NewListener 创建并绑定 Unix Datagram Socket 监听器
@@ -35,18 +36,18 @@ func NewListener(path string, log *slog.Logger) (*Listener, error) {
path: path,
conn: conn,
log: log,
buf: make([]byte, 65536),
}, nil
}
// Read 读取一条 syslog 消息,返回 tag 和 JSON 数据
func (l *Listener) Read() (string, []byte, error) {
buf := make([]byte, 65536)
n, err := l.conn.Read(buf)
n, err := l.conn.Read(l.buf)
if err != nil {
return "", nil, err
}
tag, data := ParseSyslog(buf[:n])
tag, data := ParseSyslog(l.buf[:n])
return tag, data, nil
}
+1 -3
View File
@@ -1,8 +1,6 @@
package websitestat
import (
"fmt"
ua "github.com/medama-io/go-useragent"
)
@@ -20,7 +18,7 @@ func ParseUA(rawUA string) (browser, os string) {
} else if bMajor == "" {
browser = bName
} else {
browser = fmt.Sprintf("%s %s", bName, bMajor)
browser = bName + " " + bMajor
}
// 操作系统
+46 -1
View File
@@ -25,7 +25,13 @@ useRequest(home.installedEnvironment()).onSuccess(({ data }: any) => {
})
// 统计设置
const statSetting = ref({ days: 30 })
const statSetting = ref({
days: 30,
err_buf_max: 10000,
uv_max_keys: 1000000,
ip_max_keys: 500000,
detail_max_keys: 100000
})
const statSaveLoading = ref(false)
useRequest(website.statSetting()).onSuccess(({ data }: any) => {
@@ -152,6 +158,9 @@ const handleSaveStatSetting = () => {
</n-tab-pane>
<n-tab-pane v-if="isNginx" name="stat-setting" :tab="$gettext('Statistics')">
<n-flex vertical>
<n-alert type="info">
{{ $gettext('Modifications to the limits below will take effect after restarting the panel.') }}
</n-alert>
<n-form>
<n-form-item :label="$gettext('Data Retention Days')">
<n-input-number
@@ -161,6 +170,42 @@ const handleSaveStatSetting = () => {
style="width: 200px"
/>
</n-form-item>
<n-form-item :label="$gettext('Error Buffer Max Size')">
<n-input-number
v-model:value="statSetting.err_buf_max"
:min="100"
:max="1000000"
:step="1000"
style="width: 200px"
/>
</n-form-item>
<n-form-item :label="$gettext('UV Max Keys')">
<n-input-number
v-model:value="statSetting.uv_max_keys"
:min="1000"
:max="100000000"
:step="100000"
style="width: 200px"
/>
</n-form-item>
<n-form-item :label="$gettext('IP Max Keys')">
<n-input-number
v-model:value="statSetting.ip_max_keys"
:min="1000"
:max="100000000"
:step="100000"
style="width: 200px"
/>
</n-form-item>
<n-form-item :label="$gettext('Detail Max Keys')">
<n-input-number
v-model:value="statSetting.detail_max_keys"
:min="1000"
:max="100000000"
:step="10000"
style="width: 200px"
/>
</n-form-item>
<n-button
type="primary"
:loading="statSaveLoading"
-14
View File
@@ -83,20 +83,6 @@ const sitesParam = computed(() => {
const siteOptions = ref<Array<{ label: string; value: string }>>([])
const loadSiteOptions = () => {
useRequest(website.statOverview(dateRange.value.start, dateRange.value.end, sitesParam.value))
.onSuccess(({ data }: any) => {
siteOptions.value = (data.sites || []).map((s: any) => ({
label: s.name,
value: s.name
}))
})
}
onMounted(() => {
loadSiteOptions()
})
// ============ 清空数据 ============
const handleClear = () => {
@@ -73,6 +73,11 @@ const loadOverview = () => {
)
.onSuccess(({ data }: any) => {
overview.value = data
// 回填站点选项到父组件
ctx.siteOptions.value = (data.sites || []).map((s: any) => ({
label: s.name,
value: s.name
}))
})
.onComplete(() => {
loading.value = false