diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-10 19:25:51 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-10 19:25:51 +0200 |
| commit | c43932b6eee8b8a964b9be7c21c42057f05456ba (patch) | |
| tree | 94a5a7ebeefdc79c106060c004b561985df69970 /internal | |
| parent | 70acff73c8d103ec68c575d3cac1bdd87c189cf0 (diff) | |
task 434: unify trace filter plumbing
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/eventfilter.go | 78 | ||||
| -rw-r--r-- | internal/eventloop.go | 33 | ||||
| -rw-r--r-- | internal/eventloop_constructor_test.go | 23 | ||||
| -rw-r--r-- | internal/eventloop_error_handling_test.go | 7 | ||||
| -rw-r--r-- | internal/eventloop_filter_test.go | 31 | ||||
| -rw-r--r-- | internal/globalfilter/pair.go | 5 | ||||
| -rw-r--r-- | internal/globalfilter/trace.go | 81 | ||||
| -rw-r--r-- | internal/globalfilter/trace_test.go | 47 | ||||
| -rw-r--r-- | internal/ior.go | 38 | ||||
| -rw-r--r-- | internal/ior_mode_test.go | 14 |
10 files changed, 219 insertions, 138 deletions
diff --git a/internal/eventfilter.go b/internal/eventfilter.go deleted file mode 100644 index 43a8f51..0000000 --- a/internal/eventfilter.go +++ /dev/null @@ -1,78 +0,0 @@ -package internal - -import ( - "bytes" - "fmt" - "strings" - - "ior/internal/event" - "ior/internal/types" -) - -type eventFilter struct { - commFilterEnable bool - commFilter string - commFilterBytes []byte - pathFilterEnable bool - pathFilter string - pathFilterBytes []byte -} - -func newEventFilter(commFilter, pathFilter string) (*eventFilter, error) { - var ef eventFilter - - if commFilter != "" { - if len(commFilter) > types.MAX_PROGNAME_LENGTH { - return nil, fmt.Errorf("comm filter max size is %d (got %d)", types.MAX_PROGNAME_LENGTH, len(commFilter)) - } - ef.commFilterEnable = true - ef.commFilter = commFilter - ef.commFilterBytes = []byte(commFilter) - } - - if pathFilter != "" { - if len(pathFilter) > types.MAX_FILENAME_LENGTH { - return nil, fmt.Errorf("path filter max size is %d (got %d)", types.MAX_FILENAME_LENGTH, len(pathFilter)) - } - ef.pathFilterEnable = true - ef.pathFilter = pathFilter - ef.pathFilterBytes = []byte(pathFilter) - } - - return &ef, nil -} - -func (ef *eventFilter) eventPair(ev *event.Pair) bool { - if ef.commFilterEnable && !strings.Contains(ev.Comm, ef.commFilter) { - return false - } - if ef.pathFilterEnable && (ev.File == nil || !strings.Contains(ev.File.Name(), ef.pathFilter)) { - return false - } - return true -} - -func (ef *eventFilter) openEvent(ev *types.OpenEvent) (*types.OpenEvent, bool) { - if ef.commFilterEnable && !bytes.Contains(ev.Comm[:], ef.commFilterBytes) { - return ev, false - } - - if ef.pathFilterEnable && !bytes.Contains(ev.Filename[:], ef.pathFilterBytes) { - return ev, false - } - return ev, true -} - -func (ef *eventFilter) pathEvent(ev *types.PathEvent) (*types.PathEvent, bool) { - if ef.pathFilterEnable { - return ev, bytes.Contains(ev.Pathname[:], ef.pathFilterBytes) - } - return ev, true -} - -func (ef *eventFilter) nameEvent(ev *types.NameEvent) (*types.NameEvent, bool) { - if ef.pathFilterEnable { - return ev, bytes.Contains(ev.Oldname[:], ef.pathFilterBytes) || bytes.Contains(ev.Newname[:], ef.pathFilterBytes) - } - return ev, true -} diff --git a/internal/eventloop.go b/internal/eventloop.go index 278387f..6eba594 100644 --- a/internal/eventloop.go +++ b/internal/eventloop.go @@ -14,6 +14,7 @@ import ( "ior/internal/event" "ior/internal/file" + "ior/internal/globalfilter" "ior/internal/types" ) @@ -26,8 +27,7 @@ const ( type eventLoopConfig struct { pidFilter int - commFilter string - pathFilter string + filter globalfilter.Filter collapsedFields []string countField string pprofEnable bool @@ -241,7 +241,7 @@ type rawEventHandler func(raw []byte, ch chan<- *event.Pair) type tracepointExitHandler func(ep *event.Pair) bool type eventLoop struct { - filter *eventFilter + filter globalfilter.Filter enterEvs map[uint32]*event.Pair // Temp. store of sys_enter tracepoints per Tid. pendingHandles map[uint32]string // map of TID to pathname from name_to_handle_at fdTracker *fdTracker @@ -266,13 +266,12 @@ type eventLoop struct { func newEventLoop(cfg eventLoopConfig) (*eventLoop, error) { fdState := configuredFDTracker(cfg.fdTracker) commState := configuredCommResolver(cfg.commResolver) - filter, err := newEventFilter(cfg.commFilter, cfg.pathFilter) - if err != nil { + if err := cfg.filter.ValidateTracepointFields(); err != nil { return nil, fmt.Errorf("create event filter: %w", err) } el := &eventLoop{ - filter: filter, + filter: cfg.filter.Clone(), enterEvs: make(map[uint32]*event.Pair), pendingHandles: make(map[uint32]string), fdTracker: fdState, @@ -480,8 +479,8 @@ func (e *eventLoop) initRawHandlers() { e.dropMalformedRawEvent(types.ENTER_OPEN_EVENT, raw) return } - if ev, ok := e.filter.openEvent(openEv); ok { - e.tracepointEntered(ev) + if e.filter.MatchOpenEvent(openEv) { + e.tracepointEntered(openEv) } } e.rawHandlers[types.EXIT_OPEN_EVENT] = func(raw []byte, ch chan<- *event.Pair) { @@ -538,8 +537,8 @@ func (e *eventLoop) initRawHandlers() { e.dropMalformedRawEvent(types.ENTER_NAME_EVENT, raw) return } - if ev, ok := e.filter.nameEvent(nameEv); ok { - e.tracepointEntered(ev) + if e.filter.MatchNameEvent(nameEv) { + e.tracepointEntered(nameEv) } } e.rawHandlers[types.ENTER_PATH_EVENT] = func(raw []byte, _ chan<- *event.Pair) { @@ -548,8 +547,8 @@ func (e *eventLoop) initRawHandlers() { e.dropMalformedRawEvent(types.ENTER_PATH_EVENT, raw) return } - if ev, ok := e.filter.pathEvent(pathEv); ok { - e.tracepointEntered(ev) + if e.filter.MatchPathEvent(pathEv) { + e.tracepointEntered(pathEv) } } e.rawHandlers[types.ENTER_FCNTL_EVENT] = func(raw []byte, _ chan<- *event.Pair) { @@ -582,7 +581,7 @@ func (e *eventLoop) tracepointEntered(enterEv event.Event) { tid := enterEv.GetTid() // Schedule comm lookup as early as possible to reduce races for short-lived processes. e.queueCommLookup(tid) - if !e.filter.commFilterEnable { + if !e.filter.UsesCommFilter() { e.enterEvs[tid] = event.NewPair(enterEv) return } @@ -797,7 +796,7 @@ func (e *eventLoop) handleFdExit(ep *event.Pair, fdEv *types.FdEvent) bool { } } ep.Comm = e.comm(fdEv.GetTid()) - if !e.filter.eventPair(ep) { + if !e.filter.MatchPair(ep) { ep.Recycle() return false } @@ -838,7 +837,7 @@ func (e *eventLoop) handleDup3Exit(ep *event.Pair, dup3Ev *types.Dup3Event) bool fd := int32(dup3Ev.Fd) ep.File = e.resolveFdFile(fd, dup3Ev.Pid) ep.Comm = e.comm(dup3Ev.GetTid()) - if !e.filter.eventPair(ep) { + if !e.filter.MatchPair(ep) { ep.Recycle() return false } @@ -914,7 +913,7 @@ func (e *eventLoop) handleNullExit(ep *event.Pair, nullEv *types.NullEvent) bool } } ep.Comm = e.comm(nullEv.GetTid()) - if !e.filter.eventPair(ep) { + if !e.filter.MatchPair(ep) { ep.Recycle() return false } @@ -925,7 +924,7 @@ func (e *eventLoop) handleFcntlExit(ep *event.Pair, fcntlEv *types.FcntlEvent) b ep.Comm = e.comm(fcntlEv.GetTid()) fd := int32(fcntlEv.Fd) ep.File = e.resolveFdFile(fd, fcntlEv.Pid) - if !e.filter.eventPair(ep) { + if !e.filter.MatchPair(ep) { ep.Recycle() return false } diff --git a/internal/eventloop_constructor_test.go b/internal/eventloop_constructor_test.go index 6636838..94b723e 100644 --- a/internal/eventloop_constructor_test.go +++ b/internal/eventloop_constructor_test.go @@ -4,6 +4,7 @@ import ( "strings" "testing" + "ior/internal/globalfilter" "ior/internal/types" ) @@ -19,17 +20,25 @@ func mustNewEventLoop(tb testing.TB, cfg eventLoopConfig) *eventLoop { return el } -func TestNewEventFilterRejectsTooLongCommFilter(t *testing.T) { +func TestNewEventLoopRejectsTooLongCommFilter(t *testing.T) { tooLong := strings.Repeat("a", types.MAX_PROGNAME_LENGTH+1) - _, err := newEventFilter(tooLong, "") + _, err := newEventLoop(eventLoopConfig{ + filter: globalfilter.Filter{ + Comm: &globalfilter.StringFilter{Pattern: tooLong}, + }, + }) if err == nil { t.Fatalf("expected error for comm filter longer than %d", types.MAX_PROGNAME_LENGTH) } } -func TestNewEventFilterRejectsTooLongPathFilter(t *testing.T) { +func TestNewEventLoopRejectsTooLongPathFilter(t *testing.T) { tooLong := strings.Repeat("a", types.MAX_FILENAME_LENGTH+1) - _, err := newEventFilter("", tooLong) + _, err := newEventLoop(eventLoopConfig{ + filter: globalfilter.Filter{ + File: &globalfilter.StringFilter{Pattern: tooLong}, + }, + }) if err == nil { t.Fatalf("expected error for path filter longer than %d", types.MAX_FILENAME_LENGTH) } @@ -37,7 +46,11 @@ func TestNewEventFilterRejectsTooLongPathFilter(t *testing.T) { func TestNewEventLoopPropagatesFilterError(t *testing.T) { tooLong := strings.Repeat("a", types.MAX_PROGNAME_LENGTH+1) - _, err := newEventLoop(eventLoopConfig{commFilter: tooLong}) + _, err := newEventLoop(eventLoopConfig{ + filter: globalfilter.Filter{ + Comm: &globalfilter.StringFilter{Pattern: tooLong}, + }, + }) if err == nil { t.Fatalf("expected newEventLoop to propagate invalid filter error") } diff --git a/internal/eventloop_error_handling_test.go b/internal/eventloop_error_handling_test.go index e025343..b5add72 100644 --- a/internal/eventloop_error_handling_test.go +++ b/internal/eventloop_error_handling_test.go @@ -4,6 +4,7 @@ import ( "testing" "ior/internal/event" + "ior/internal/globalfilter" "ior/internal/types" ) @@ -146,7 +147,11 @@ func TestProcessRawEventMalformedKnownTypeDoesNotPanicAndNotifies(t *testing.T) } func TestTracepointEnteredMissingCommWithCommFilterNotifies(t *testing.T) { - el := mustNewEventLoop(t, eventLoopConfig{commFilter: "system"}) + el := mustNewEventLoop(t, eventLoopConfig{ + filter: globalfilter.Filter{ + Comm: &globalfilter.StringFilter{Pattern: "system"}, + }, + }) warnings := make(chan string, 1) el.warningCb = func(message string) { warnings <- message } diff --git a/internal/eventloop_filter_test.go b/internal/eventloop_filter_test.go index b31f39c..ec8e75b 100644 --- a/internal/eventloop_filter_test.go +++ b/internal/eventloop_filter_test.go @@ -8,6 +8,7 @@ import ( "ior/internal/event" "ior/internal/file" + "ior/internal/globalfilter" "ior/internal/types" ) @@ -449,9 +450,7 @@ func TestCommFilterToggle(t *testing.T) { // Create eventloop without comm filter el := &eventLoop{ - filter: &eventFilter{ - commFilterEnable: false, - }, + filter: globalfilter.Filter{}, enterEvs: make(map[uint32]*event.Pair), fdTracker: newFDTracker(make(map[int32]file.File)), commResolver: newCommResolver(make(map[uint32]string)), @@ -493,10 +492,8 @@ func TestCommFilterToggle(t *testing.T) { // Create eventloop with comm filter enabled el := &eventLoop{ - filter: &eventFilter{ - commFilterEnable: true, - commFilter: "test", - commFilterBytes: []byte("test"), + filter: globalfilter.Filter{ + Comm: &globalfilter.StringFilter{Pattern: "test"}, }, enterEvs: make(map[uint32]*event.Pair), fdTracker: newFDTracker(make(map[int32]file.File)), @@ -532,14 +529,7 @@ func TestCommFilterToggle(t *testing.T) { func newEventLoopWithFilter(commFilter, pathFilter string) *eventLoop { el := &eventLoop{ - filter: &eventFilter{ - commFilterEnable: commFilter != "", - commFilter: commFilter, - commFilterBytes: []byte(commFilter), - pathFilterEnable: pathFilter != "", - pathFilter: pathFilter, - pathFilterBytes: []byte(pathFilter), - }, + filter: testFilter(commFilter, pathFilter), enterEvs: make(map[uint32]*event.Pair), fdTracker: newFDTracker(make(map[int32]file.File)), commResolver: newCommResolver(make(map[uint32]string)), @@ -550,3 +540,14 @@ func newEventLoopWithFilter(commFilter, pathFilter string) *eventLoop { } return el } + +func testFilter(commFilter, pathFilter string) globalfilter.Filter { + filter := globalfilter.Filter{} + if commFilter != "" { + filter.Comm = &globalfilter.StringFilter{Pattern: commFilter} + } + if pathFilter != "" { + filter.File = &globalfilter.StringFilter{Pattern: pathFilter} + } + return filter +} diff --git a/internal/globalfilter/pair.go b/internal/globalfilter/pair.go index 4215120..7aca331 100644 --- a/internal/globalfilter/pair.go +++ b/internal/globalfilter/pair.go @@ -6,10 +6,7 @@ import ( ) func MatchPair(filter Filter, pair *event.Pair) bool { - if pair == nil { - return false - } - return filter.Matches(pairCandidate{pair: pair}) + return filter.MatchPair(pair) } type pairCandidate struct { diff --git a/internal/globalfilter/trace.go b/internal/globalfilter/trace.go new file mode 100644 index 0000000..b1e3fde --- /dev/null +++ b/internal/globalfilter/trace.go @@ -0,0 +1,81 @@ +package globalfilter + +import ( + "fmt" + "strings" + + "ior/internal/event" + "ior/internal/types" +) + +// ValidateTracepointFields checks string filters that are applied against +// fixed-size kernel event fields before pair reconstruction. +func (f Filter) ValidateTracepointFields() error { + if err := validateTraceStringFilter("comm", f.Comm, types.MAX_PROGNAME_LENGTH); err != nil { + return err + } + if err := validateTraceStringFilter("path", f.File, types.MAX_FILENAME_LENGTH); err != nil { + return err + } + return nil +} + +// UsesCommFilter reports whether the filter needs command-name matching. +func (f Filter) UsesCommFilter() bool { + return hasStringPattern(f.Comm) +} + +// MatchPair reports whether a completed syscall pair satisfies the filter. +func (f Filter) MatchPair(pair *event.Pair) bool { + if pair == nil { + return false + } + return f.Matches(pairCandidate{pair: pair}) +} + +// MatchOpenEvent applies the subset of the filter that can be evaluated on raw +// open events before exit pairing. +func (f Filter) MatchOpenEvent(ev *types.OpenEvent) bool { + if ev == nil { + return false + } + if !matchString(f.Comm, types.StringValue(ev.Comm[:])) { + return false + } + return matchString(f.File, types.StringValue(ev.Filename[:])) +} + +// MatchPathEvent applies the path-related subset of the filter to raw path events. +func (f Filter) MatchPathEvent(ev *types.PathEvent) bool { + if ev == nil { + return false + } + return matchString(f.File, types.StringValue(ev.Pathname[:])) +} + +// MatchNameEvent applies the path-related subset of the filter to raw rename-like events. +func (f Filter) MatchNameEvent(ev *types.NameEvent) bool { + if ev == nil { + return false + } + if !hasStringPattern(f.File) { + return true + } + return matchString(f.File, types.StringValue(ev.Oldname[:])) || + matchString(f.File, types.StringValue(ev.Newname[:])) +} + +func hasStringPattern(filter *StringFilter) bool { + return filter != nil && strings.TrimSpace(filter.Pattern) != "" +} + +func validateTraceStringFilter(name string, filter *StringFilter, maxLen int) error { + if !hasStringPattern(filter) { + return nil + } + pattern := strings.TrimSpace(filter.Pattern) + if len(pattern) > maxLen { + return fmt.Errorf("%s filter max size is %d (got %d)", name, maxLen, len(pattern)) + } + return nil +} diff --git a/internal/globalfilter/trace_test.go b/internal/globalfilter/trace_test.go new file mode 100644 index 0000000..cd5736a --- /dev/null +++ b/internal/globalfilter/trace_test.go @@ -0,0 +1,47 @@ +package globalfilter + +import ( + "strings" + "testing" + + "ior/internal/types" +) + +func TestValidateTracepointFieldsRejectsOversizedPatterns(t *testing.T) { + tooLongComm := strings.Repeat("a", types.MAX_PROGNAME_LENGTH+1) + if err := (Filter{Comm: &StringFilter{Pattern: tooLongComm}}).ValidateTracepointFields(); err == nil { + t.Fatalf("expected oversized comm pattern to fail validation") + } + + tooLongPath := strings.Repeat("b", types.MAX_FILENAME_LENGTH+1) + if err := (Filter{File: &StringFilter{Pattern: tooLongPath}}).ValidateTracepointFields(); err == nil { + t.Fatalf("expected oversized path pattern to fail validation") + } +} + +func TestTracepointHelpersMatchRawEvents(t *testing.T) { + filter := Filter{ + Comm: &StringFilter{Pattern: "^nginx"}, + File: &StringFilter{Pattern: "access.log$"}, + } + + open := &types.OpenEvent{} + copy(open.Comm[:], "nginx-worker") + copy(open.Filename[:], "/var/log/nginx/access.log") + if !filter.MatchOpenEvent(open) { + t.Fatalf("expected open event to match comm and file filters") + } + + path := &types.PathEvent{} + copy(path.Pathname[:], "/var/log/nginx/access.log") + if !filter.MatchPathEvent(path) { + t.Fatalf("expected path event to match file filter") + } + + name := &types.NameEvent{} + copy(name.Oldname[:], "/tmp/old.log") + copy(name.Newname[:], "/var/log/nginx/access.log") + if !filter.MatchNameEvent(name) { + t.Fatalf("expected rename event to match file filter via newname") + } +} diff --git a/internal/ior.go b/internal/ior.go index 668dda1..7b8f68a 100644 --- a/internal/ior.go +++ b/internal/ior.go @@ -177,7 +177,7 @@ func tuiTraceStarterFromRunTrace( cfg := baseCfg if filter, ok := tui.TraceFiltersFromContext(ctx); ok { cfg.GlobalFilter = filter.Clone() - applyTraceFilterConfig(&cfg, filter) + applyTraceScopeFromGlobalFilter(&cfg, filter) } engine := statsengine.NewEngine(64) streamBuf := eventstream.NewRingBuffer() @@ -243,24 +243,15 @@ func shouldIngestTracePair(filter globalfilter.Filter, pair *event.Pair) bool { if !filter.IsActive() { return true } - return globalfilter.MatchPair(filter, pair) + return filter.MatchPair(pair) } -func applyTraceFilterConfig(cfg *flags.Config, filter globalfilter.Filter) { +func applyTraceScopeFromGlobalFilter(cfg *flags.Config, filter globalfilter.Filter) { if cfg == nil { return } - cfg.CommFilter = "" - cfg.PathFilter = "" cfg.PidFilter = -1 cfg.TidFilter = -1 - - if filter.Comm != nil { - cfg.CommFilter = filter.Comm.Pattern - } - if filter.File != nil { - cfg.PathFilter = filter.File.Pattern - } if pid, ok := eqFilterValue(filter.PID); ok { cfg.PidFilter = pid } @@ -285,8 +276,7 @@ func newEventLoopConfig(cfg flags.Config) eventLoopConfig { copy(fields, cfg.CollapsedFields) return eventLoopConfig{ pidFilter: cfg.PidFilter, - commFilter: cfg.CommFilter, - pathFilter: cfg.PathFilter, + filter: traceFilterFromConfig(cfg), collapsedFields: fields, countField: cfg.CountField, pprofEnable: cfg.PprofEnable, @@ -294,6 +284,26 @@ func newEventLoopConfig(cfg flags.Config) eventLoopConfig { } } +func traceFilterFromConfig(cfg flags.Config) globalfilter.Filter { + filter := cfg.GlobalFilter.Clone() + if filter.IsActive() { + return filter + } + if cfg.CommFilter != "" { + filter.Comm = &globalfilter.StringFilter{Pattern: cfg.CommFilter} + } + if cfg.PathFilter != "" { + filter.File = &globalfilter.StringFilter{Pattern: cfg.PathFilter} + } + if cfg.PidFilter > 0 { + filter.PID = &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: int64(cfg.PidFilter)} + } + if cfg.TidFilter > 0 { + filter.TID = &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: int64(cfg.TidFilter)} + } + return filter +} + type profilingControl struct { done chan struct{} enabled bool diff --git a/internal/ior_mode_test.go b/internal/ior_mode_test.go index 4140485..8fbc79c 100644 --- a/internal/ior_mode_test.go +++ b/internal/ior_mode_test.go @@ -461,11 +461,17 @@ func TestTuiTraceStarterFromRunTraceUsesContextFilters(t *testing.T) { if gotCfg.TidFilter != 3333 { t.Fatalf("expected tid filter from context, got %d", gotCfg.TidFilter) } - if gotCfg.CommFilter != "nginx" { - t.Fatalf("expected comm filter from context, got %q", gotCfg.CommFilter) + if gotCfg.CommFilter != "" { + t.Fatalf("expected legacy comm filter to remain unused, got %q", gotCfg.CommFilter) } - if gotCfg.PathFilter != "/var/log" { - t.Fatalf("expected path filter from context, got %q", gotCfg.PathFilter) + if gotCfg.PathFilter != "" { + t.Fatalf("expected legacy path filter to remain unused, got %q", gotCfg.PathFilter) + } + if gotCfg.GlobalFilter.Comm == nil || gotCfg.GlobalFilter.Comm.Pattern != "nginx" { + t.Fatalf("expected comm preserved in global filter payload, got %+v", gotCfg.GlobalFilter.Comm) + } + if gotCfg.GlobalFilter.File == nil || gotCfg.GlobalFilter.File.Pattern != "/var/log" { + t.Fatalf("expected file preserved in global filter payload, got %+v", gotCfg.GlobalFilter.File) } if gotCfg.GlobalFilter.Syscall == nil || gotCfg.GlobalFilter.Syscall.Pattern != "read" { t.Fatalf("expected syscall preserved in global filter payload, got %+v", gotCfg.GlobalFilter.Syscall) |
