mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
feat(access-logs): 明细详情支持 IP 分析与 URL Tab 记忆
- 新增单 IP 分析接口,趋势时间范围支持至 30 天 - 明细详情弹窗展示趋势、汇总与 Top 分布,可快捷管理 IP 组 - 访问日志页签改为 URL 参数记忆,筛选后保持当前 Tab
This commit is contained in:
@@ -22,7 +22,7 @@ const (
|
||||
defaultAccessLogFoldMinute = 3
|
||||
defaultIPTrendHours = 24
|
||||
defaultIPTrendBucketMinute = 30
|
||||
maxIPTrendHours = 168
|
||||
maxIPTrendHours = 24 * 30
|
||||
nodeAccessLogRetentionDays = 90
|
||||
defaultAccessLogQueryDays = 7
|
||||
accessLogFieldRemoteAddr = "remote_addr"
|
||||
@@ -181,6 +181,38 @@ type AccessLogIPTrendView struct {
|
||||
Points []AccessLogIPTrendPoint `json:"points"`
|
||||
}
|
||||
|
||||
// AccessLogIPAnalysisQuery filters per-IP analysis queries.
|
||||
type AccessLogIPAnalysisQuery struct {
|
||||
NodeID string `json:"node_id"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
Host string `json:"host"`
|
||||
Hours int `json:"hours"`
|
||||
}
|
||||
|
||||
// AccessLogIPAnalysisSummary is headline totals for one IP.
|
||||
type AccessLogIPAnalysisSummary struct {
|
||||
TotalRequests int64 `json:"total_requests"`
|
||||
ErrorCount int64 `json:"error_count"`
|
||||
BandwidthServed int64 `json:"bandwidth_served"`
|
||||
BytesReceived int64 `json:"bytes_received"`
|
||||
UniqueHosts int64 `json:"unique_hosts"`
|
||||
UniquePaths int64 `json:"unique_paths"`
|
||||
}
|
||||
|
||||
// AccessLogIPAnalysisView is per-IP analytics for the detail dialog.
|
||||
type AccessLogIPAnalysisView struct {
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
Hours int `json:"hours"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
Summary AccessLogIPAnalysisSummary `json:"summary"`
|
||||
TopPaths []DistributionItem `json:"top_paths"`
|
||||
TopHosts []DistributionItem `json:"top_hosts"`
|
||||
StatusCodes []DistributionItem `json:"status_codes"`
|
||||
TopUserAgents []DistributionItem `json:"top_user_agents"`
|
||||
DeviceTypes []DistributionItem `json:"device_types"`
|
||||
TopBrowsers []DistributionItem `json:"top_browsers"`
|
||||
}
|
||||
|
||||
// AccessLogOverviewQuery filters access log overview queries.
|
||||
type AccessLogOverviewQuery struct {
|
||||
NodeID string `json:"node_id"`
|
||||
@@ -665,6 +697,82 @@ func GetAccessLogIPTrend(ctx context.Context, input AccessLogIPTrendQuery) (*Acc
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAccessLogIPAnalysis returns per-IP summary and rankings.
|
||||
func GetAccessLogIPAnalysis(ctx context.Context, input AccessLogIPAnalysisQuery) (*AccessLogIPAnalysisView, error) {
|
||||
normalized, err := normalizeAccessLogIPAnalysisQuery(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
since := now.Add(-time.Duration(normalized.Hours) * time.Hour)
|
||||
query := model.OpenFlareAccessLogQuery{
|
||||
NodeID: normalized.NodeID,
|
||||
RemoteAddr: normalized.RemoteAddr,
|
||||
Host: normalized.Host,
|
||||
Since: since,
|
||||
Until: now,
|
||||
}
|
||||
|
||||
summaryRow, err := model.TrafficSummaryOpenFlareAccessLogs(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allPaths := valueCountDistribution(ctx, query, "path", 0)
|
||||
allHosts := valueCountDistribution(ctx, query, "host", 0)
|
||||
topPaths := limitDistributionItems(allPaths, accessLogOverviewTopLimit)
|
||||
topHosts := limitDistributionItems(allHosts, accessLogOverviewTopLimit)
|
||||
statusCodes := valueCountDistribution(ctx, query, "status_code", accessLogOverviewTopLimit)
|
||||
topUserAgents := valueCountDistribution(ctx, query, "user_agent", accessLogOverviewTopLimit)
|
||||
deviceTypes, topBrowsers, _, _ := buildAccessLogUADistributions(ctx, query)
|
||||
|
||||
return &AccessLogIPAnalysisView{
|
||||
RemoteAddr: normalized.RemoteAddr,
|
||||
Hours: normalized.Hours,
|
||||
GeneratedAt: now,
|
||||
Summary: AccessLogIPAnalysisSummary{
|
||||
TotalRequests: summaryRow.RequestCount,
|
||||
ErrorCount: summaryRow.ErrorCount,
|
||||
BandwidthServed: summaryRow.BytesSent,
|
||||
BytesReceived: summaryRow.RequestLength,
|
||||
UniqueHosts: int64(len(allHosts)),
|
||||
UniquePaths: int64(len(allPaths)),
|
||||
},
|
||||
TopPaths: topPaths,
|
||||
TopHosts: topHosts,
|
||||
StatusCodes: statusCodes,
|
||||
TopUserAgents: topUserAgents,
|
||||
DeviceTypes: deviceTypes,
|
||||
TopBrowsers: topBrowsers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func limitDistributionItems(items []DistributionItem, limit int) []DistributionItem {
|
||||
if limit <= 0 || len(items) <= limit {
|
||||
return items
|
||||
}
|
||||
return items[:limit]
|
||||
}
|
||||
|
||||
func normalizeAccessLogIPAnalysisQuery(input AccessLogIPAnalysisQuery) (AccessLogIPAnalysisQuery, error) {
|
||||
remoteAddr := strings.TrimSpace(input.RemoteAddr)
|
||||
if remoteAddr == "" {
|
||||
return AccessLogIPAnalysisQuery{}, errors.New("remote_addr 不能为空")
|
||||
}
|
||||
hours := input.Hours
|
||||
if hours <= 0 {
|
||||
hours = defaultIPTrendHours
|
||||
}
|
||||
if hours > maxIPTrendHours {
|
||||
hours = maxIPTrendHours
|
||||
}
|
||||
return AccessLogIPAnalysisQuery{
|
||||
NodeID: strings.TrimSpace(input.NodeID),
|
||||
RemoteAddr: remoteAddr,
|
||||
Host: strings.TrimSpace(input.Host),
|
||||
Hours: hours,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CleanupAccessLogs removes access logs older than retention days.
|
||||
func CleanupAccessLogs(ctx context.Context, input AccessLogCleanupInput) (*AccessLogCleanupResult, error) {
|
||||
if input.RetentionDays <= 0 || input.RetentionDays > nodeAccessLogRetentionDays {
|
||||
|
||||
@@ -205,6 +205,35 @@ func GetAccessLogIPTrendHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response.OK(result))
|
||||
}
|
||||
|
||||
// GetAccessLogIPAnalysisHandler 获取单 IP 访问分析。
|
||||
// @Summary 获取访问日志 IP 分析
|
||||
// @Description 返回指定 IP 的汇总指标与 Top 分布,需要管理员权限
|
||||
// @Tags openflare-observability
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param node_id query string false "节点 ID"
|
||||
// @Param remote_addr query string false "客户端 IP"
|
||||
// @Param host query string false "请求 Host"
|
||||
// @Param hours query int false "统计时间范围(小时)"
|
||||
// @Success 200 {object} response.Any{data=observability.AccessLogIPAnalysisView} "IP 访问分析"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 404 {object} response.Any "无权限或不存在"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/d/access-logs/ip-summary/analysis [get]
|
||||
func GetAccessLogIPAnalysisHandler(c *gin.Context) {
|
||||
result, err := GetAccessLogIPAnalysis(c.Request.Context(), AccessLogIPAnalysisQuery{
|
||||
NodeID: c.Query("node_id"),
|
||||
RemoteAddr: c.Query("remote_addr"),
|
||||
Host: c.Query("host"),
|
||||
Hours: readQueryInt(c, "hours"),
|
||||
})
|
||||
if apiutil.AbortBadRequestOnError(c, err) {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(result))
|
||||
}
|
||||
|
||||
// CleanupAccessLogsHandler 清理过期访问日志。
|
||||
// @Summary 清理访问日志
|
||||
// @Description 按保留天数清理过期访问日志记录,需要管理员权限
|
||||
|
||||
Reference in New Issue
Block a user