mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
b56290d79d
Overview API accepts bucket_minutes (5|60); rate-limit analysis uses 5-minute buckets and drops 15d/30d presets. Widen rank value column.
302 lines
12 KiB
Go
302 lines
12 KiB
Go
// Copyright 2026 Arctel.net
|
||
// SPDX-License-Identifier: Apache-2.0
|
||
|
||
package observability
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
|
||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
|
||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// GetAccessLogOverviewHandler 获取访问日志概览。
|
||
// @Summary 获取访问日志概览
|
||
// @Description 返回访问日志汇总指标、趋势与 Top 排行,需要管理员权限
|
||
// @Tags openflare-observability
|
||
// @Produce json
|
||
// @Security SessionCookie
|
||
// @Param node_id query string false "节点 ID"
|
||
// @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 "未登录"
|
||
// @Failure 404 {object} response.Any "无权限或不存在"
|
||
// @Failure 500 {object} response.Any "内部错误"
|
||
// @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"),
|
||
BucketMinutes: readQueryInt(c, "bucket_minutes"),
|
||
})
|
||
if apiutil.AbortBadRequestOnError(c, err) {
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, response.OK(result))
|
||
}
|
||
|
||
// GetAccessLogsHandler 分页列出访问日志。
|
||
// @Summary 列出访问日志
|
||
// @Description 分页返回 OpenFlare 访问日志,支持按节点、IP、主机与路径筛选,需要管理员权限
|
||
// @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 path query string false "请求路径"
|
||
// @Param p query int false "页码"
|
||
// @Param page_size query int false "每页条数"
|
||
// @Param sort_by query string false "排序字段"
|
||
// @Param sort_order query string false "排序方向"
|
||
// @Success 200 {object} response.Any{data=observability.AccessLogList} "访问日志列表"
|
||
// @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 [get]
|
||
func GetAccessLogsHandler(c *gin.Context) {
|
||
logs, err := ListAccessLogs(c.Request.Context(), readAccessLogQuery(c))
|
||
if apiutil.AbortBadRequestOnError(c, err) {
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, response.OK(logs))
|
||
}
|
||
|
||
// GetFoldedAccessLogsHandler 分页列出折叠访问日志。
|
||
// @Summary 列出折叠访问日志
|
||
// @Description 按时间桶聚合访问日志并分页返回,需要管理员权限
|
||
// @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 path query string false "请求路径"
|
||
// @Param fold_minutes query int false "折叠时间窗口(分钟)"
|
||
// @Param p query int false "页码"
|
||
// @Param page_size query int false "每页条数"
|
||
// @Param sort_by query string false "排序字段"
|
||
// @Param sort_order query string false "排序方向"
|
||
// @Success 200 {object} response.Any{data=observability.FoldedAccessLogList} "折叠访问日志列表"
|
||
// @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/folds [get]
|
||
func GetFoldedAccessLogsHandler(c *gin.Context) {
|
||
query := readAccessLogQuery(c)
|
||
query.FoldMinutes = readQueryInt(c, "fold_minutes")
|
||
logs, err := ListFoldedAccessLogs(c.Request.Context(), query)
|
||
if apiutil.AbortBadRequestOnError(c, err) {
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, response.OK(logs))
|
||
}
|
||
|
||
// GetFoldedAccessLogIPsHandler 列出折叠桶内的 IP 汇总。
|
||
// @Summary 列出折叠访问日志 IP 汇总
|
||
// @Description 在指定时间桶内按 IP 聚合访问统计,需要管理员权限
|
||
// @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 path query string false "请求路径"
|
||
// @Param bucket_started_at query string false "时间桶起始时间"
|
||
// @Param fold_minutes query int false "折叠时间窗口(分钟)"
|
||
// @Param p query int false "页码"
|
||
// @Param page_size query int false "每页条数"
|
||
// @Param sort_by query string false "排序字段"
|
||
// @Param sort_order query string false "排序方向"
|
||
// @Success 200 {object} response.Any{data=observability.FoldedAccessLogIPList} "折叠 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/folds/ip-summary [get]
|
||
func GetFoldedAccessLogIPsHandler(c *gin.Context) {
|
||
result, err := ListFoldedAccessLogIPs(c.Request.Context(), FoldedAccessLogIPQuery{
|
||
NodeID: c.Query("node_id"),
|
||
RemoteAddr: c.Query("remote_addr"),
|
||
Host: c.Query("host"),
|
||
Path: c.Query("path"),
|
||
BucketStartedAt: c.Query("bucket_started_at"),
|
||
FoldMinutes: readQueryInt(c, "fold_minutes"),
|
||
Page: readQueryInt(c, "p"),
|
||
PageSize: readQueryInt(c, "page_size"),
|
||
SortBy: c.Query("sort_by"),
|
||
SortOrder: c.Query("sort_order"),
|
||
})
|
||
if apiutil.AbortBadRequestOnError(c, err) {
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, response.OK(result))
|
||
}
|
||
|
||
// GetAccessLogIPSummariesHandler 列出访问日志 IP 汇总。
|
||
// @Summary 列出访问日志 IP 汇总
|
||
// @Description 按 IP 聚合访问日志统计并分页返回;支持 hours 或 since/until 时间窗,需要管理员权限
|
||
// @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 "统计时间范围(小时,1-720,默认 168)"
|
||
// @Param since query string false "开始时间 RFC3339(与 until 同时提供时优先于 hours)"
|
||
// @Param until query string false "结束时间 RFC3339"
|
||
// @Param p query int false "页码"
|
||
// @Param page_size query int false "每页条数"
|
||
// @Param sort_by query string false "排序字段 total_requests|request_length|bytes_sent|success_ratio|last_seen_at|remote_addr"
|
||
// @Param sort_order query string false "排序方向"
|
||
// @Success 200 {object} response.Any{data=observability.AccessLogIPSummaryList} "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 [get]
|
||
func GetAccessLogIPSummariesHandler(c *gin.Context) {
|
||
result, err := ListAccessLogIPSummaries(c.Request.Context(), AccessLogIPSummaryQuery{
|
||
NodeID: c.Query("node_id"),
|
||
RemoteAddr: c.Query("remote_addr"),
|
||
Host: c.Query("host"),
|
||
Hours: readQueryInt(c, "hours"),
|
||
Since: c.Query("since"),
|
||
Until: c.Query("until"),
|
||
Page: readQueryInt(c, "p"),
|
||
PageSize: readQueryInt(c, "page_size"),
|
||
SortBy: c.Query("sort_by"),
|
||
SortOrder: c.Query("sort_order"),
|
||
})
|
||
if apiutil.AbortBadRequestOnError(c, err) {
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, response.OK(result))
|
||
}
|
||
|
||
// GetAccessLogIPTrendHandler 获取 IP 访问趋势。
|
||
// @Summary 获取访问日志 IP 趋势
|
||
// @Description 返回指定 IP 在时间范围内的访问趋势数据,需要管理员权限
|
||
// @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 "统计时间范围(小时)"
|
||
// @Param bucket_minutes query int false "时间桶粒度(分钟)"
|
||
// @Success 200 {object} response.Any{data=observability.AccessLogIPTrendView} "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/trend [get]
|
||
func GetAccessLogIPTrendHandler(c *gin.Context) {
|
||
result, err := GetAccessLogIPTrend(c.Request.Context(), AccessLogIPTrendQuery{
|
||
NodeID: c.Query("node_id"),
|
||
RemoteAddr: c.Query("remote_addr"),
|
||
Host: c.Query("host"),
|
||
Hours: readQueryInt(c, "hours"),
|
||
BucketMinutes: readQueryInt(c, "bucket_minutes"),
|
||
})
|
||
if apiutil.AbortBadRequestOnError(c, err) {
|
||
return
|
||
}
|
||
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 按保留天数清理过期访问日志记录,需要管理员权限
|
||
// @Tags openflare-observability
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Security SessionCookie
|
||
// @Param request body observability.AccessLogCleanupInput true "清理参数"
|
||
// @Success 200 {object} response.Any{data=observability.AccessLogCleanupResult} "清理结果"
|
||
// @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/cleanup [post]
|
||
func CleanupAccessLogsHandler(c *gin.Context) {
|
||
var input AccessLogCleanupInput
|
||
if !apiutil.BindJSON(c, &input) {
|
||
return
|
||
}
|
||
result, err := CleanupAccessLogs(c.Request.Context(), input)
|
||
if apiutil.AbortBadRequestOnError(c, err) {
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, response.OK(result))
|
||
}
|
||
|
||
func readAccessLogQuery(c *gin.Context) AccessLogQuery {
|
||
return AccessLogQuery{
|
||
NodeID: c.Query("node_id"),
|
||
RemoteAddr: c.Query("remote_addr"),
|
||
Host: c.Query("host"),
|
||
Path: c.Query("path"),
|
||
Page: readQueryInt(c, "p"),
|
||
PageSize: readQueryInt(c, "page_size"),
|
||
SortBy: c.Query("sort_by"),
|
||
SortOrder: c.Query("sort_order"),
|
||
}
|
||
}
|
||
|
||
func readQueryInt(c *gin.Context, key string) int {
|
||
value, _ := strconv.Atoi(c.DefaultQuery(key, "0"))
|
||
return value
|
||
}
|
||
|
||
// readQueryStringArray reads repeated query values for key, and also accepts
|
||
// the Axios/jQuery bracket form key[] / key%5B%5D which Gin does not map to key.
|
||
func readQueryStringArray(c *gin.Context, key string) []string {
|
||
if values := c.QueryArray(key); len(values) > 0 {
|
||
return values
|
||
}
|
||
if values := c.QueryArray(key + "[]"); len(values) > 0 {
|
||
return values
|
||
}
|
||
return nil
|
||
}
|