Implement filter in status, website and uri in CLI and Frontend
This commit is contained in:
175
cmd/frontend/filter.go
Normal file
175
cmd/frontend/filter.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
st "git.ipng.ch/ipng/nginx-logtail/internal/store"
|
||||
)
|
||||
|
||||
// andRe splits a filter expression on AND (case-insensitive, surrounded by whitespace).
|
||||
var andRe = regexp.MustCompile(`(?i)\s+and\s+`)
|
||||
|
||||
// ParseFilterExpr parses a mini filter expression into a filterState.
|
||||
//
|
||||
// Syntax: TERM [AND TERM ...]
|
||||
//
|
||||
// Terms:
|
||||
//
|
||||
// status=200 status!=200 status>=400 status>400 status<=500 status<500
|
||||
// website=example.com — exact match
|
||||
// website~=gouda.* — RE2 regex
|
||||
// uri=/api/v1/ — exact match
|
||||
// uri~=^/api/.* — RE2 regex
|
||||
// prefix=1.2.3.0/24 — exact match
|
||||
//
|
||||
// Values may be enclosed in double or single quotes.
|
||||
func ParseFilterExpr(s string) (filterState, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return filterState{}, nil
|
||||
}
|
||||
terms := andRe.Split(s, -1)
|
||||
var fs filterState
|
||||
for _, term := range terms {
|
||||
term = strings.TrimSpace(term)
|
||||
if term == "" {
|
||||
continue
|
||||
}
|
||||
if err := applyTerm(term, &fs); err != nil {
|
||||
return filterState{}, err
|
||||
}
|
||||
}
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
// applyTerm parses a single "field op value" term into fs.
|
||||
func applyTerm(term string, fs *filterState) error {
|
||||
// Find the first operator character: ~, !, >, <, =
|
||||
opIdx := strings.IndexAny(term, "~!><=")
|
||||
if opIdx <= 0 {
|
||||
return fmt.Errorf("invalid term %q: expected field=value, field>=value, field~=regex, etc.", term)
|
||||
}
|
||||
|
||||
field := strings.ToLower(strings.TrimSpace(term[:opIdx]))
|
||||
rest := term[opIdx:]
|
||||
|
||||
var op, value string
|
||||
switch {
|
||||
case strings.HasPrefix(rest, "~="):
|
||||
op, value = "~=", rest[2:]
|
||||
case strings.HasPrefix(rest, "!="):
|
||||
op, value = "!=", rest[2:]
|
||||
case strings.HasPrefix(rest, ">="):
|
||||
op, value = ">=", rest[2:]
|
||||
case strings.HasPrefix(rest, "<="):
|
||||
op, value = "<=", rest[2:]
|
||||
case strings.HasPrefix(rest, ">"):
|
||||
op, value = ">", rest[1:]
|
||||
case strings.HasPrefix(rest, "<"):
|
||||
op, value = "<", rest[1:]
|
||||
case strings.HasPrefix(rest, "="):
|
||||
op, value = "=", rest[1:]
|
||||
default:
|
||||
return fmt.Errorf("unrecognised operator in %q", term)
|
||||
}
|
||||
|
||||
value = unquote(strings.TrimSpace(value))
|
||||
|
||||
switch field {
|
||||
case "status":
|
||||
if op == "~=" {
|
||||
return fmt.Errorf("status does not support ~=; use =, !=, >=, >, <=, <")
|
||||
}
|
||||
expr := op + value
|
||||
if op == "=" {
|
||||
expr = value // ParseStatusExpr accepts bare "200"
|
||||
}
|
||||
if _, _, ok := st.ParseStatusExpr(expr); !ok {
|
||||
return fmt.Errorf("invalid status expression %q", expr)
|
||||
}
|
||||
fs.Status = expr
|
||||
case "website":
|
||||
switch op {
|
||||
case "=":
|
||||
fs.Website = value
|
||||
case "~=":
|
||||
fs.WebsiteRe = value
|
||||
default:
|
||||
return fmt.Errorf("website only supports = and ~=, not %q", op)
|
||||
}
|
||||
case "uri":
|
||||
switch op {
|
||||
case "=":
|
||||
fs.URI = value
|
||||
case "~=":
|
||||
fs.URIRe = value
|
||||
default:
|
||||
return fmt.Errorf("uri only supports = and ~=, not %q", op)
|
||||
}
|
||||
case "prefix":
|
||||
if op != "=" {
|
||||
return fmt.Errorf("prefix only supports =, not %q", op)
|
||||
}
|
||||
fs.Prefix = value
|
||||
default:
|
||||
return fmt.Errorf("unknown field %q; valid: status, website, uri, prefix", field)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// unquote strips surrounding double or single quotes.
|
||||
func unquote(s string) string {
|
||||
if len(s) >= 2 {
|
||||
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
|
||||
return s[1 : len(s)-1]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// FilterExprString serialises a filterState back into the mini filter expression language.
|
||||
// Returns "" when no filters are active.
|
||||
func FilterExprString(f filterState) string {
|
||||
var parts []string
|
||||
if f.Website != "" {
|
||||
parts = append(parts, "website="+quoteMaybe(f.Website))
|
||||
}
|
||||
if f.WebsiteRe != "" {
|
||||
parts = append(parts, "website~="+quoteMaybe(f.WebsiteRe))
|
||||
}
|
||||
if f.Prefix != "" {
|
||||
parts = append(parts, "prefix="+quoteMaybe(f.Prefix))
|
||||
}
|
||||
if f.URI != "" {
|
||||
parts = append(parts, "uri="+quoteMaybe(f.URI))
|
||||
}
|
||||
if f.URIRe != "" {
|
||||
parts = append(parts, "uri~="+quoteMaybe(f.URIRe))
|
||||
}
|
||||
if f.Status != "" {
|
||||
parts = append(parts, statusTermStr(f.Status))
|
||||
}
|
||||
return strings.Join(parts, " AND ")
|
||||
}
|
||||
|
||||
// statusTermStr converts a stored status expression (">=400", "200") to a
|
||||
// full filter term ("status>=400", "status=200").
|
||||
func statusTermStr(expr string) string {
|
||||
if expr == "" {
|
||||
return ""
|
||||
}
|
||||
if len(expr) > 0 && (expr[0] == '!' || expr[0] == '>' || expr[0] == '<') {
|
||||
return "status" + expr
|
||||
}
|
||||
return "status=" + expr
|
||||
}
|
||||
|
||||
// quoteMaybe wraps s in double quotes when it contains spaces or quote characters.
|
||||
func quoteMaybe(s string) string {
|
||||
if strings.ContainsAny(s, " \t\"'") {
|
||||
return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"`
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user