2

Improve requests log filters and sorting

This commit is contained in:
2026-03-12 18:42:12 +01:00
parent 200fc83831
commit fef2237c49
7 changed files with 589 additions and 89 deletions

View File

@@ -9,6 +9,7 @@ import (
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
@@ -257,18 +258,123 @@ func overviewFilterQueryParts(options model.OverviewOptions) (joins []string, cl
return joins, clauses
}
func eventFilterQueryParts(options model.EventListOptions) (joins []string, clauses []string) {
func normalizedEventStateExpression() string {
return `CASE WHEN COALESCE(s.state, '') = '' THEN '` + string(model.IPStateObserved) + `' ELSE s.state END`
}
func parseStatusFilter(filter string) (clauses []string, args []any, err error) {
normalized := strings.ToLower(strings.TrimSpace(filter))
if normalized == "" {
return nil, nil, nil
}
if len(normalized) == 3 && strings.HasSuffix(normalized, "xx") && normalized[0] >= '1' && normalized[0] <= '5' {
hundred := int(normalized[0]-'0') * 100
return []string{`e.status >= ?`, `e.status < ?`}, []any{hundred, hundred + 100}, nil
}
status, convErr := strconv.Atoi(normalized)
if convErr != nil || status < 100 || status > 599 {
return nil, nil, fmt.Errorf("invalid status filter %q", filter)
}
return []string{`e.status = ?`}, []any{status}, nil
}
func eventOrderClause(options model.EventListOptions) string {
stateExpr := normalizedEventStateExpression()
direction := "ASC"
tieDirection := "ASC"
field := strings.ToLower(strings.TrimSpace(options.SortBy))
if field == "" {
return `e.occurred_at DESC, e.id DESC`
}
if options.SortDesc {
direction = "DESC"
tieDirection = "DESC"
}
switch field {
case "time", "occurred_at":
return fmt.Sprintf(`e.occurred_at %s, e.id %s`, direction, tieDirection)
case "source":
return fmt.Sprintf(`LOWER(e.source_name) %s, e.occurred_at %s, e.id %s`, direction, tieDirection, tieDirection)
case "ip":
return fmt.Sprintf(`e.client_ip %s, e.occurred_at %s, e.id %s`, direction, tieDirection, tieDirection)
case "method":
return fmt.Sprintf(`UPPER(e.method) %s, e.occurred_at %s, e.id %s`, direction, tieDirection, tieDirection)
case "request", "uri":
return fmt.Sprintf(`e.uri %s, e.host %s, e.occurred_at %s, e.id %s`, direction, direction, tieDirection, tieDirection)
case "status":
return fmt.Sprintf(`e.status %s, e.occurred_at %s, e.id %s`, direction, tieDirection, tieDirection)
case "state":
return fmt.Sprintf(`%s %s, e.occurred_at %s, e.id %s`, stateExpr, direction, tieDirection, tieDirection)
case "reason":
return fmt.Sprintf(`LOWER(e.decision_reason) %s, e.occurred_at %s, e.id %s`, direction, tieDirection, tieDirection)
default:
return `e.occurred_at DESC, e.id DESC`
}
}
func eventFilterQueryParts(options model.EventListOptions) (joins []string, clauses []string, args []any, err error) {
joins = append(joins, `LEFT JOIN ip_state s ON s.ip = e.client_ip`)
if !options.ShowAllowed {
clauses = append(clauses, `COALESCE(s.state, '') <> '`+string(model.IPStateAllowed)+`'`)
stateExpr := normalizedEventStateExpression()
stateFilter := strings.ToLower(strings.TrimSpace(options.State))
if stateFilter == "all" {
stateFilter = ""
}
if options.ReviewOnly {
clauses = append(clauses, `COALESCE(s.state, '') = '`+string(model.IPStateReview)+`'`)
if stateFilter != "" {
switch stateFilter {
case string(model.IPStateObserved), string(model.IPStateReview), string(model.IPStateBlocked), string(model.IPStateAllowed):
clauses = append(clauses, stateExpr+` = ?`)
args = append(args, stateFilter)
default:
return nil, nil, nil, fmt.Errorf("invalid state filter %q", options.State)
}
} else {
if !options.ShowAllowed {
clauses = append(clauses, stateExpr+` <> ?`)
args = append(args, string(model.IPStateAllowed))
}
if options.ReviewOnly {
clauses = append(clauses, stateExpr+` = ?`)
args = append(args, string(model.IPStateReview))
}
}
if !options.ShowKnownBots {
if source := strings.TrimSpace(options.Source); source != "" {
clauses = append(clauses, `LOWER(e.source_name) = LOWER(?)`)
args = append(args, source)
}
if method := strings.TrimSpace(options.Method); method != "" {
if strings.EqualFold(method, "other") {
clauses = append(clauses, `UPPER(COALESCE(e.method, '')) NOT IN ('GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS')`)
} else {
clauses = append(clauses, `UPPER(COALESCE(e.method, '')) = ?`)
args = append(args, strings.ToUpper(method))
}
}
statusClauses, statusArgs, statusErr := parseStatusFilter(options.StatusFilter)
if statusErr != nil {
return nil, nil, nil, statusErr
}
clauses = append(clauses, statusClauses...)
args = append(args, statusArgs...)
botFilter := strings.ToLower(strings.TrimSpace(options.BotFilter))
if botFilter == "" && !options.ShowKnownBots {
botFilter = "no-known"
}
switch botFilter {
case "", "all":
case "known":
clauses = append(clauses, knownBotExistsClause(`e.client_ip`))
case "possible":
clauses = append(clauses, anyBotExistsClause(`e.client_ip`), `NOT `+knownBotExistsClause(`e.client_ip`))
case "any":
clauses = append(clauses, anyBotExistsClause(`e.client_ip`))
case "non-bot":
clauses = append(clauses, `NOT `+anyBotExistsClause(`e.client_ip`))
case "no-known":
clauses = append(clauses, `NOT `+knownBotExistsClause(`e.client_ip`))
default:
return nil, nil, nil, fmt.Errorf("invalid bot filter %q", options.BotFilter)
}
return joins, clauses
return joins, clauses, args, nil
}
func (s *Store) AddDecision(ctx context.Context, decision *model.DecisionRecord) error {
@@ -836,7 +942,10 @@ func (s *Store) ListEvents(ctx context.Context, since time.Time, limit int, opti
if options.Offset < 0 {
options.Offset = 0
}
joins, clauses := eventFilterQueryParts(options)
joins, clauses, filterArgs, err := eventFilterQueryParts(options)
if err != nil {
return nil, err
}
query := `
SELECT e.id, e.source_name, e.profile_name, e.occurred_at, e.remote_ip, e.client_ip, e.host,
e.method, e.uri, e.path, e.status, e.user_agent, e.decision, e.decision_reason,
@@ -846,15 +955,16 @@ func (s *Store) ListEvents(ctx context.Context, since time.Time, limit int, opti
if len(joins) > 0 {
query += ` ` + strings.Join(joins, ` `)
}
args := make([]any, 0, 2)
args := make([]any, 0, len(filterArgs)+3)
if !since.IsZero() {
clauses = append([]string{`e.occurred_at >= ?`}, clauses...)
args = append(args, formatTime(since))
}
args = append(args, filterArgs...)
if len(clauses) > 0 {
query += ` WHERE ` + strings.Join(clauses, ` AND `)
}
query += ` ORDER BY e.occurred_at DESC, e.id DESC LIMIT ? OFFSET ?`
query += ` ORDER BY ` + eventOrderClause(options) + ` LIMIT ? OFFSET ?`
args = append(args, limit, options.Offset)
rows, err := s.db.QueryContext(ctx, query, args...)