176 lines
4.4 KiB
Go
176 lines
4.4 KiB
Go
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
|
|
}
|