feat(access-logs): 状态码支持手动输入,新增时间范围筛选

- 状态码筛选支持预设快捷选项 + 手动输入任意 100-599 状态码(数字校验)
- 新增时间范围筛选:shadcn 日期+时间选择器(Popover+Calendar+时分 Select),
  起止时间以 RFC3339 成对传入,后端校验格式与先后关系,非法值返回 400
- 默认显示来源 IP/访问域名/状态码,节点 ID/请求路径/时间范围折叠进「更多筛选」
This commit is contained in:
ryan
2026-08-13 10:27:40 +08:00
parent 75d1fcf345
commit a9e4237bbf
11 changed files with 309 additions and 36 deletions
@@ -40,6 +40,8 @@ type AccessLogQuery struct {
Host string `json:"host"`
Path string `json:"path"`
StatusCode int `json:"status_code"`
Since string `json:"since"`
Until string `json:"until"`
Page int `json:"page"`
PageSize int `json:"page_size"`
SortBy string `json:"sort_by"`
@@ -534,7 +536,10 @@ func buildAccessLogOverviewTrends(
// ListAccessLogs returns paginated access logs.
func ListAccessLogs(ctx context.Context, input AccessLogQuery) (*AccessLogList, error) {
normalized := normalizeAccessLogQuery(input)
modelQuery := buildModelAccessLogQuery(normalized)
modelQuery, err := buildModelAccessLogQuery(normalized)
if err != nil {
return nil, err
}
logs, err := repository.ListOpenFlareAccessLogs(ctx, modelQuery)
if err != nil {
return nil, err
@@ -587,13 +592,17 @@ func ListFoldedAccessLogs(ctx context.Context, input AccessLogQuery) (*FoldedAcc
if err != nil {
return nil, err
}
modelQuery := buildModelAccessLogQuery(normalized)
modelQuery, err := buildModelAccessLogQuery(normalized)
if err != nil {
return nil, err
}
bucketQuery := model.OpenFlareAccessLogBucketQuery{
NodeID: modelQuery.NodeID,
RemoteAddr: modelQuery.RemoteAddr,
Host: modelQuery.Host,
Path: modelQuery.Path,
Since: modelQuery.Since,
Until: modelQuery.Until,
Page: normalized.Page,
PageSize: normalized.PageSize,
SortBy: normalizeFoldSortBy(input.SortBy),
@@ -882,19 +891,49 @@ func CleanupAccessLogs(ctx context.Context, input AccessLogCleanupInput) (*Acces
}, nil
}
func buildModelAccessLogQuery(input AccessLogQuery) model.OpenFlareAccessLogQuery {
func buildModelAccessLogQuery(input AccessLogQuery) (model.OpenFlareAccessLogQuery, error) {
since := defaultAccessLogSince()
until := time.Now().UTC()
if input.Since != "" || input.Until != "" {
parsedSince, parsedUntil, err := resolveAccessLogWindow(input.Since, input.Until)
if err != nil {
return model.OpenFlareAccessLogQuery{}, err
}
since, until = parsedSince, parsedUntil
}
return model.OpenFlareAccessLogQuery{
NodeID: strings.TrimSpace(input.NodeID),
RemoteAddr: strings.TrimSpace(input.RemoteAddr),
Host: strings.TrimSpace(input.Host),
Path: strings.TrimSpace(input.Path),
StatusCode: input.StatusCode,
Since: defaultAccessLogSince(),
Since: since,
Until: until,
Page: input.Page,
PageSize: input.PageSize,
SortBy: input.SortBy,
SortOrder: input.SortOrder,
}, nil
}
// resolveAccessLogWindow 校验并解析 RFC3339 时间窗;since/until 必须成对提供,
// 且 until 需晚于 since。
func resolveAccessLogWindow(sinceRaw, untilRaw string) (time.Time, time.Time, error) {
if sinceRaw == "" || untilRaw == "" {
return time.Time{}, time.Time{}, errors.New("since 与 until 需同时提供")
}
parsedSince, err := time.Parse(time.RFC3339, sinceRaw)
if err != nil {
return time.Time{}, time.Time{}, errors.New("since 时间格式无效,需为 RFC3339")
}
parsedUntil, err := time.Parse(time.RFC3339, untilRaw)
if err != nil {
return time.Time{}, time.Time{}, errors.New("until 时间格式无效,需为 RFC3339")
}
if !parsedUntil.After(parsedSince) {
return time.Time{}, time.Time{}, errors.New("until 必须晚于 since")
}
return parsedSince, parsedUntil, nil
}
func defaultAccessLogSince() time.Time {
@@ -935,6 +974,8 @@ func normalizeAccessLogQuery(input AccessLogQuery) AccessLogQuery {
Host: strings.TrimSpace(input.Host),
Path: strings.TrimSpace(input.Path),
StatusCode: input.StatusCode,
Since: strings.TrimSpace(input.Since),
Until: strings.TrimSpace(input.Until),
Page: normalizeAccessLogPage(input.Page),
PageSize: normalizeAccessLogPageSize(input.PageSize),
SortBy: normalizeAccessLogSortBy(input.SortBy),
@@ -101,3 +101,30 @@ func TestReadAccessLogQueryRejectsInvalidStatusCode(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 0, got.StatusCode)
}
func TestResolveAccessLogWindow(t *testing.T) {
since, until, err := resolveAccessLogWindow(
"2026-08-01T00:00:00Z",
"2026-08-02T00:00:00Z",
)
require.NoError(t, err)
require.True(t, until.After(since))
for _, tc := range []struct {
name string
since string
until string
}{
{name: "missing both", since: "", until: ""},
{name: "only since", since: "2026-08-01T00:00:00Z", until: ""},
{name: "only until", since: "", until: "2026-08-02T00:00:00Z"},
{name: "bad since", since: "not-a-time", until: "2026-08-02T00:00:00Z"},
{name: "bad until", since: "2026-08-01T00:00:00Z", until: "not-a-time"},
{name: "reversed", since: "2026-08-02T00:00:00Z", until: "2026-08-01T00:00:00Z"},
} {
t.Run(tc.name, func(t *testing.T) {
_, _, err := resolveAccessLogWindow(tc.since, tc.until)
require.Error(t, err)
})
}
}
@@ -54,6 +54,8 @@ func GetAccessLogOverviewHandler(c *gin.Context) {
// @Param host query string false "请求 Host"
// @Param path query string false "请求路径"
// @Param status_code query int false "HTTP 状态码(100-599)"
// @Param since query string false "起始时间(RFC3339,需与 until 成对提供)"
// @Param until query string false "结束时间(RFC3339,需与 since 成对提供)"
// @Param p query int false "页码"
// @Param page_size query int false "每页条数"
// @Param sort_by query string false "排序字段"
@@ -284,6 +286,8 @@ func readAccessLogQuery(c *gin.Context) (AccessLogQuery, error) {
RemoteAddr: c.Query("remote_addr"),
Host: c.Query("host"),
Path: c.Query("path"),
Since: c.Query("since"),
Until: c.Query("until"),
Page: readQueryInt(c, "p"),
PageSize: readQueryInt(c, "page_size"),
SortBy: c.Query("sort_by"),