Implement filter in status, website and uri in CLI and Frontend

This commit is contained in:
2026-03-14 21:59:30 +01:00
parent 2962590a74
commit afa65a2b29
15 changed files with 1159 additions and 123 deletions

View File

@@ -4,7 +4,8 @@ package store
import (
"container/heap"
"fmt"
"log"
"regexp"
"time"
pb "git.ipng.ch/ipng/nginx-logtail/proto/logtailpb"
@@ -113,27 +114,101 @@ func indexOf(s string, b byte) int {
// --- filtering and grouping ---
// CompiledFilter wraps a pb.Filter with pre-compiled regular expressions.
// Use CompileFilter to construct one before a query loop.
type CompiledFilter struct {
Proto *pb.Filter
WebsiteRe *regexp.Regexp // nil if no website_regex or compilation failed
URIRe *regexp.Regexp // nil if no uri_regex or compilation failed
}
// CompileFilter compiles the regex fields in f once. Invalid regexes are
// logged and treated as "match nothing" for that field.
func CompileFilter(f *pb.Filter) *CompiledFilter {
cf := &CompiledFilter{Proto: f}
if f == nil {
return cf
}
if f.WebsiteRegex != nil {
re, err := regexp.Compile(f.GetWebsiteRegex())
if err != nil {
log.Printf("store: invalid website_regex %q: %v", f.GetWebsiteRegex(), err)
} else {
cf.WebsiteRe = re
}
}
if f.UriRegex != nil {
re, err := regexp.Compile(f.GetUriRegex())
if err != nil {
log.Printf("store: invalid uri_regex %q: %v", f.GetUriRegex(), err)
} else {
cf.URIRe = re
}
}
return cf
}
// MatchesFilter returns true if t satisfies all constraints in f.
// A nil filter matches everything.
func MatchesFilter(t Tuple4, f *pb.Filter) bool {
if f == nil {
func MatchesFilter(t Tuple4, f *CompiledFilter) bool {
if f == nil || f.Proto == nil {
return true
}
if f.Website != nil && t.Website != f.GetWebsite() {
p := f.Proto
if p.Website != nil && t.Website != p.GetWebsite() {
return false
}
if f.ClientPrefix != nil && t.Prefix != f.GetClientPrefix() {
if f.WebsiteRe != nil && !f.WebsiteRe.MatchString(t.Website) {
return false
}
if f.HttpRequestUri != nil && t.URI != f.GetHttpRequestUri() {
// website_regex set but failed to compile → match nothing
if p.WebsiteRegex != nil && f.WebsiteRe == nil {
return false
}
if f.HttpResponse != nil && t.Status != fmt.Sprint(f.GetHttpResponse()) {
if p.ClientPrefix != nil && t.Prefix != p.GetClientPrefix() {
return false
}
if p.HttpRequestUri != nil && t.URI != p.GetHttpRequestUri() {
return false
}
if f.URIRe != nil && !f.URIRe.MatchString(t.URI) {
return false
}
if p.UriRegex != nil && f.URIRe == nil {
return false
}
if p.HttpResponse != nil && !matchesStatusOp(t.Status, p.GetHttpResponse(), p.StatusOp) {
return false
}
return true
}
// matchesStatusOp applies op(statusStr, want), parsing statusStr as an integer.
// Returns false if statusStr is not a valid integer.
func matchesStatusOp(statusStr string, want int32, op pb.StatusOp) bool {
var got int32
for _, c := range []byte(statusStr) {
if c < '0' || c > '9' {
return false
}
got = got*10 + int32(c-'0')
}
switch op {
case pb.StatusOp_NE:
return got != want
case pb.StatusOp_GT:
return got > want
case pb.StatusOp_GE:
return got >= want
case pb.StatusOp_LT:
return got < want
case pb.StatusOp_LE:
return got <= want
default: // EQ
return got == want
}
}
// DimensionLabel returns the string value of t for the given group-by dimension.
func DimensionLabel(t Tuple4, g pb.GroupBy) string {
switch g {
@@ -150,6 +225,45 @@ func DimensionLabel(t Tuple4, g pb.GroupBy) string {
}
}
// ParseStatusExpr parses a status filter expression into a value and operator.
// Accepted syntax: 200, =200, ==200, !=200, >400, >=400, <500, <=500.
// Returns ok=false if the expression is empty or unparseable.
func ParseStatusExpr(s string) (value int32, op pb.StatusOp, ok bool) {
if s == "" {
return 0, pb.StatusOp_EQ, false
}
var digits string
switch {
case len(s) >= 2 && s[:2] == "!=":
op, digits = pb.StatusOp_NE, s[2:]
case len(s) >= 2 && s[:2] == ">=":
op, digits = pb.StatusOp_GE, s[2:]
case len(s) >= 2 && s[:2] == "<=":
op, digits = pb.StatusOp_LE, s[2:]
case len(s) >= 2 && s[:2] == "==":
op, digits = pb.StatusOp_EQ, s[2:]
case s[0] == '>':
op, digits = pb.StatusOp_GT, s[1:]
case s[0] == '<':
op, digits = pb.StatusOp_LT, s[1:]
case s[0] == '=':
op, digits = pb.StatusOp_EQ, s[1:]
default:
op, digits = pb.StatusOp_EQ, s
}
var n int32
if digits == "" {
return 0, pb.StatusOp_EQ, false
}
for _, c := range []byte(digits) {
if c < '0' || c > '9' {
return 0, pb.StatusOp_EQ, false
}
n = n*10 + int32(c-'0')
}
return n, op, true
}
// --- heap-based top-K selection ---
type entryHeap []Entry