feat(rate-limits): use 5m trend buckets and 24h/7d ranges

Overview API accepts bucket_minutes (5|60); rate-limit analysis uses
5-minute buckets and drops 15d/30d presets. Widen rank value column.
This commit is contained in:
ryan
2026-07-19 19:18:15 +08:00
parent 86d2d6b0ad
commit b56290d79d
10 changed files with 90 additions and 39 deletions
@@ -232,10 +232,11 @@ type AccessLogIPAnalysisView struct {
// AccessLogOverviewQuery filters access log overview queries.
type AccessLogOverviewQuery struct {
NodeID string `json:"node_id"`
Host string `json:"host"`
Hosts []string `json:"hosts"`
Hours int `json:"hours"`
NodeID string `json:"node_id"`
Host string `json:"host"`
Hosts []string `json:"hosts"`
Hours int `json:"hours"`
BucketMinutes int `json:"bucket_minutes"`
}
// AccessLogOverviewMetricPoint is a single overview trend bucket.
@@ -262,6 +263,7 @@ type AccessLogOverviewTrends struct {
type AccessLogOverview struct {
GeneratedAt time.Time `json:"generated_at"`
Hours int `json:"hours"`
BucketMinutes int `json:"bucket_minutes"`
Summary AccessLogOverviewSummary `json:"summary"`
Trends AccessLogOverviewTrends `json:"trends"`
TopPaths []DistributionItem `json:"top_paths"`
@@ -287,10 +289,11 @@ type AccessLogCleanupResult struct {
}
const (
defaultAccessLogOverviewHours = 24
maxAccessLogOverviewHours = 24 * 30
accessLogOverviewTopLimit = 10
accessLogOverviewUASampleLimit = 200
defaultAccessLogOverviewHours = 24
maxAccessLogOverviewHours = 24 * 30
defaultAccessLogOverviewBucketMinutes = 60
accessLogOverviewTopLimit = 10
accessLogOverviewUASampleLimit = 200
)
// GetAccessLogOverview returns summary metrics, trends, and top rankings.
@@ -312,13 +315,14 @@ func GetAccessLogOverview(ctx context.Context, input AccessLogOverviewQuery) (*A
}
requestPoints, visitPoints, bandwidthPoints := buildAccessLogOverviewTrends(
ctx, now, normalized.Hours, query,
ctx, now, normalized.Hours, normalized.BucketMinutes, query,
)
deviceTypes, topBrowsers, topOSes, topUserAgents := buildAccessLogUADistributions(ctx, query)
return &AccessLogOverview{
GeneratedAt: now,
Hours: normalized.Hours,
GeneratedAt: now,
Hours: normalized.Hours,
BucketMinutes: normalized.BucketMinutes,
Summary: AccessLogOverviewSummary{
TotalRequests: summaryRow.RequestCount,
TotalVisits: summaryRow.UniqueIPCount,
@@ -348,6 +352,7 @@ func normalizeAccessLogOverviewQuery(input AccessLogOverviewQuery) AccessLogOver
if hours > maxAccessLogOverviewHours {
hours = maxAccessLogOverviewHours
}
bucketMinutes := normalizeAccessLogOverviewBucketMinutes(input.BucketMinutes)
hosts := normalizeAccessLogOverviewHosts(input.Hosts)
host := strings.TrimSpace(input.Host)
if len(hosts) == 0 && host != "" {
@@ -359,10 +364,20 @@ func normalizeAccessLogOverviewQuery(input AccessLogOverviewQuery) AccessLogOver
host = ""
}
return AccessLogOverviewQuery{
NodeID: strings.TrimSpace(input.NodeID),
Host: host,
Hosts: hosts,
Hours: hours,
NodeID: strings.TrimSpace(input.NodeID),
Host: host,
Hosts: hosts,
Hours: hours,
BucketMinutes: bucketMinutes,
}
}
func normalizeAccessLogOverviewBucketMinutes(value int) int {
switch value {
case 5, 60:
return value
default:
return defaultAccessLogOverviewBucketMinutes
}
}
@@ -454,6 +469,7 @@ func buildAccessLogOverviewTrends(
ctx context.Context,
now time.Time,
hours int,
bucketMinutes int,
query model.OpenFlareAccessLogQuery,
) (
requests []AccessLogOverviewMetricPoint,
@@ -463,12 +479,18 @@ func buildAccessLogOverviewTrends(
if hours <= 0 {
hours = defaultAccessLogOverviewHours
}
start := now.Truncate(time.Hour).Add(-time.Duration(hours-1) * time.Hour)
requests = make([]AccessLogOverviewMetricPoint, hours)
visits = make([]AccessLogOverviewMetricPoint, hours)
bandwidth = make([]AccessLogOverviewMetricPoint, hours)
bucketMinutes = normalizeAccessLogOverviewBucketMinutes(bucketMinutes)
bucketDuration := time.Duration(bucketMinutes) * time.Minute
bucketCount := hours * 60 / bucketMinutes
if bucketCount <= 0 {
bucketCount = 1
}
start := now.Truncate(bucketDuration).Add(-time.Duration(bucketCount-1) * bucketDuration)
requests = make([]AccessLogOverviewMetricPoint, bucketCount)
visits = make([]AccessLogOverviewMetricPoint, bucketCount)
bandwidth = make([]AccessLogOverviewMetricPoint, bucketCount)
for index := range requests {
bucketAt := start.Add(time.Duration(index) * time.Hour)
bucketAt := start.Add(time.Duration(index) * bucketDuration)
requests[index].BucketStartedAt = bucketAt
visits[index].BucketStartedAt = bucketAt
bandwidth[index].BucketStartedAt = bucketAt
@@ -480,7 +502,7 @@ func buildAccessLogOverviewTrends(
Hosts: query.Hosts,
Since: query.Since,
Until: query.Until,
FoldMinutes: 60,
FoldMinutes: bucketMinutes,
SortBy: defaultAccessLogSortBy,
SortOrder: sortOrderAsc,
})
@@ -22,6 +22,7 @@ import (
// @Param host query string false "请求 Host(单域名)"
// @Param hosts query []string false "请求 Host 列表(多域名精确匹配)"
// @Param hours query int false "统计时间范围(小时)"
// @Param bucket_minutes query int false "趋势桶分钟数(5 或 60,默认 60)"
// @Success 200 {object} response.Any{data=observability.AccessLogOverview} "访问日志概览"
// @Failure 400 {object} response.Any "参数错误"
// @Failure 401 {object} response.Any "未登录"
@@ -30,10 +31,11 @@ import (
// @Router /api/v1/d/access-logs/overview [get]
func GetAccessLogOverviewHandler(c *gin.Context) {
result, err := GetAccessLogOverview(c.Request.Context(), AccessLogOverviewQuery{
NodeID: c.Query("node_id"),
Host: c.Query("host"),
Hosts: readQueryStringArray(c, "hosts"),
Hours: readQueryInt(c, "hours"),
NodeID: c.Query("node_id"),
Host: c.Query("host"),
Hosts: readQueryStringArray(c, "hosts"),
Hours: readQueryInt(c, "hours"),
BucketMinutes: readQueryInt(c, "bucket_minutes"),
})
if apiutil.AbortBadRequestOnError(c, err) {
return