mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 01:12:17 +08:00
feat: 完善网站统计
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 将时长字符串转换为秒数
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 纯静态虚拟主机
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
// 操作系统
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user