summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/globalfilter/filter.go272
-rw-r--r--internal/globalfilter/filter_test.go144
-rw-r--r--internal/tui/eventstream/filter.go230
-rw-r--r--internal/tui/eventstream/model.go37
-rw-r--r--internal/tui/eventstream/streamevent.go44
5 files changed, 475 insertions, 252 deletions
diff --git a/internal/globalfilter/filter.go b/internal/globalfilter/filter.go
new file mode 100644
index 0000000..b9072a7
--- /dev/null
+++ b/internal/globalfilter/filter.go
@@ -0,0 +1,272 @@
+package globalfilter
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+)
+
+type CompareOp int
+
+const (
+ OpEq CompareOp = iota
+ OpNeq
+ OpGt
+ OpGte
+ OpLt
+ OpLte
+)
+
+type NumericFilter struct {
+ Op CompareOp
+ Value int64
+}
+
+type StringFilter struct {
+ Pattern string
+}
+
+type Candidate interface {
+ SyscallValue() string
+ CommValue() string
+ FileValue() string
+ PIDValue() uint32
+ TIDValue() uint32
+ FDValue() int32
+ LatencyValue() uint64
+ GapValue() uint64
+ BytesValue() uint64
+ ReturnValue() int64
+ ErrorValue() bool
+}
+
+type Filter struct {
+ Syscall *StringFilter
+ Comm *StringFilter
+ File *StringFilter
+ PID *NumericFilter
+ TID *NumericFilter
+ FD *NumericFilter
+ LatencyNs *NumericFilter
+ GapNs *NumericFilter
+ Bytes *NumericFilter
+ RetVal *NumericFilter
+ ErrorsOnly bool
+}
+
+func (f Filter) Clone() Filter {
+ out := f
+ out.Syscall = cloneStringFilter(f.Syscall)
+ out.Comm = cloneStringFilter(f.Comm)
+ out.File = cloneStringFilter(f.File)
+ out.PID = cloneNumericFilter(f.PID)
+ out.TID = cloneNumericFilter(f.TID)
+ out.FD = cloneNumericFilter(f.FD)
+ out.LatencyNs = cloneNumericFilter(f.LatencyNs)
+ out.GapNs = cloneNumericFilter(f.GapNs)
+ out.Bytes = cloneNumericFilter(f.Bytes)
+ out.RetVal = cloneNumericFilter(f.RetVal)
+ return out
+}
+
+func (f Filter) Matches(candidate Candidate) bool {
+ if candidate == nil {
+ return false
+ }
+ if f.ErrorsOnly && !candidate.ErrorValue() {
+ return false
+ }
+ if !matchString(f.Syscall, candidate.SyscallValue()) {
+ return false
+ }
+ if !matchString(f.Comm, candidate.CommValue()) {
+ return false
+ }
+ if !matchString(f.File, candidate.FileValue()) {
+ return false
+ }
+ if !matchNumeric(f.PID, int64(candidate.PIDValue())) {
+ return false
+ }
+ if !matchNumeric(f.TID, int64(candidate.TIDValue())) {
+ return false
+ }
+ if !matchNumeric(f.FD, int64(candidate.FDValue())) {
+ return false
+ }
+ if !matchNumeric(f.LatencyNs, int64(candidate.LatencyValue())) {
+ return false
+ }
+ if !matchNumeric(f.GapNs, int64(candidate.GapValue())) {
+ return false
+ }
+ if !matchNumeric(f.Bytes, int64(candidate.BytesValue())) {
+ return false
+ }
+ if !matchNumeric(f.RetVal, candidate.ReturnValue()) {
+ return false
+ }
+ return true
+}
+
+func (f Filter) IsActive() bool {
+ if f.ErrorsOnly {
+ return true
+ }
+ for _, sf := range []*StringFilter{f.Syscall, f.Comm, f.File} {
+ if sf != nil && strings.TrimSpace(sf.Pattern) != "" {
+ return true
+ }
+ }
+ for _, nf := range []*NumericFilter{f.PID, f.TID, f.FD, f.LatencyNs, f.GapNs, f.Bytes, f.RetVal} {
+ if nf != nil {
+ return true
+ }
+ }
+ return false
+}
+
+func (f Filter) Summary() string {
+ parts := make([]string, 0, 10)
+ if f.ErrorsOnly {
+ parts = append(parts, "errors")
+ }
+ parts = appendStringSummary(parts, "syscall", f.Syscall)
+ parts = appendStringSummary(parts, "comm", f.Comm)
+ parts = appendStringSummary(parts, "file", f.File)
+ parts = appendNumericSummary(parts, "pid", f.PID, false)
+ parts = appendNumericSummary(parts, "tid", f.TID, false)
+ parts = appendNumericSummary(parts, "fd", f.FD, false)
+ parts = appendNumericSummary(parts, "latency", f.LatencyNs, true)
+ parts = appendNumericSummary(parts, "gap", f.GapNs, true)
+ parts = appendNumericSummary(parts, "bytes", f.Bytes, false)
+ parts = appendNumericSummary(parts, "ret", f.RetVal, false)
+ if len(parts) == 0 {
+ return "all"
+ }
+ return strings.Join(parts, " ")
+}
+
+func ParseDurationNs(input string) (int64, error) {
+ s := strings.TrimSpace(strings.ToLower(input))
+ if s == "" {
+ return 0, fmt.Errorf("empty duration")
+ }
+ s = strings.ReplaceAll(s, "µs", "us")
+ s = strings.ReplaceAll(s, "μs", "us")
+ if onlyDigits(s) || strings.HasPrefix(s, "-") && onlyDigits(s[1:]) {
+ v, err := strconv.ParseInt(s, 10, 64)
+ if err != nil {
+ return 0, err
+ }
+ return v, nil
+ }
+ d, err := time.ParseDuration(s)
+ if err != nil {
+ return 0, err
+ }
+ return d.Nanoseconds(), nil
+}
+
+func appendStringSummary(parts []string, name string, sf *StringFilter) []string {
+ if sf == nil {
+ return parts
+ }
+ pattern := strings.TrimSpace(sf.Pattern)
+ if pattern == "" {
+ return parts
+ }
+ return append(parts, fmt.Sprintf("%s~%s", name, pattern))
+}
+
+func appendNumericSummary(parts []string, name string, nf *NumericFilter, duration bool) []string {
+ if nf == nil {
+ return parts
+ }
+ value := strconv.FormatInt(nf.Value, 10)
+ if duration {
+ value = time.Duration(nf.Value).String()
+ }
+ return append(parts, fmt.Sprintf("%s%s%s", name, compareOpSymbol(nf.Op), value))
+}
+
+func matchString(sf *StringFilter, value string) bool {
+ if sf == nil {
+ return true
+ }
+ pattern := strings.ToLower(strings.TrimSpace(sf.Pattern))
+ if pattern == "" {
+ return true
+ }
+ return strings.Contains(strings.ToLower(value), pattern)
+}
+
+func matchNumeric(nf *NumericFilter, value int64) bool {
+ if nf == nil {
+ return true
+ }
+ switch nf.Op {
+ case OpEq:
+ return value == nf.Value
+ case OpNeq:
+ return value != nf.Value
+ case OpGt:
+ return value > nf.Value
+ case OpGte:
+ return value >= nf.Value
+ case OpLt:
+ return value < nf.Value
+ case OpLte:
+ return value <= nf.Value
+ default:
+ return false
+ }
+}
+
+func compareOpSymbol(op CompareOp) string {
+ switch op {
+ case OpEq:
+ return "="
+ case OpNeq:
+ return "!="
+ case OpGt:
+ return ">"
+ case OpGte:
+ return ">="
+ case OpLt:
+ return "<"
+ case OpLte:
+ return "<="
+ default:
+ return "?"
+ }
+}
+
+func cloneStringFilter(in *StringFilter) *StringFilter {
+ if in == nil {
+ return nil
+ }
+ out := *in
+ return &out
+}
+
+func cloneNumericFilter(in *NumericFilter) *NumericFilter {
+ if in == nil {
+ return nil
+ }
+ out := *in
+ return &out
+}
+
+func onlyDigits(s string) bool {
+ if s == "" {
+ return false
+ }
+ for _, ch := range s {
+ if ch < '0' || ch > '9' {
+ return false
+ }
+ }
+ return true
+}
diff --git a/internal/globalfilter/filter_test.go b/internal/globalfilter/filter_test.go
new file mode 100644
index 0000000..ff04ea7
--- /dev/null
+++ b/internal/globalfilter/filter_test.go
@@ -0,0 +1,144 @@
+package globalfilter
+
+import (
+ "strings"
+ "testing"
+)
+
+type sampleCandidate struct {
+ syscall string
+ comm string
+ file string
+ pid uint32
+ tid uint32
+ fd int32
+ latency uint64
+ gap uint64
+ bytes uint64
+ ret int64
+ isError bool
+}
+
+func (s sampleCandidate) SyscallValue() string { return s.syscall }
+func (s sampleCandidate) CommValue() string { return s.comm }
+func (s sampleCandidate) FileValue() string { return s.file }
+func (s sampleCandidate) PIDValue() uint32 { return s.pid }
+func (s sampleCandidate) TIDValue() uint32 { return s.tid }
+func (s sampleCandidate) FDValue() int32 { return s.fd }
+func (s sampleCandidate) LatencyValue() uint64 { return s.latency }
+func (s sampleCandidate) GapValue() uint64 { return s.gap }
+func (s sampleCandidate) BytesValue() uint64 { return s.bytes }
+func (s sampleCandidate) ReturnValue() int64 { return s.ret }
+func (s sampleCandidate) ErrorValue() bool { return s.isError }
+
+func testCandidate() sampleCandidate {
+ return sampleCandidate{
+ syscall: "read",
+ comm: "nginx",
+ file: "/var/log/access.log",
+ pid: 1234,
+ tid: 1235,
+ fd: 7,
+ latency: 1_500_000,
+ gap: 12_000,
+ bytes: 4_096,
+ ret: -1,
+ isError: true,
+ }
+}
+
+func TestFilterZeroValueMatchesAll(t *testing.T) {
+ candidate := testCandidate()
+ filter := Filter{}
+ if !filter.Matches(candidate) {
+ t.Fatalf("zero-value filter should match all candidates")
+ }
+ if filter.IsActive() {
+ t.Fatalf("zero-value filter should be inactive")
+ }
+ if got := filter.Summary(); got != "all" {
+ t.Fatalf("Summary() = %q, want all", got)
+ }
+}
+
+func TestFilterStringAndNumericMatching(t *testing.T) {
+ candidate := testCandidate()
+ filter := Filter{
+ Syscall: &StringFilter{Pattern: "ea"},
+ Comm: &StringFilter{Pattern: "NGI"},
+ File: &StringFilter{Pattern: "access"},
+ PID: &NumericFilter{Op: OpEq, Value: 1234},
+ TID: &NumericFilter{Op: OpNeq, Value: 1},
+ FD: &NumericFilter{Op: OpEq, Value: 7},
+ LatencyNs: &NumericFilter{Op: OpGt, Value: 1_000_000},
+ GapNs: &NumericFilter{Op: OpLte, Value: 12_000},
+ Bytes: &NumericFilter{Op: OpLt, Value: 8_192},
+ RetVal: &NumericFilter{Op: OpGte, Value: -1},
+ }
+ if !filter.Matches(candidate) {
+ t.Fatalf("combined filter should match candidate")
+ }
+}
+
+func TestFilterErrorsOnlyAndClone(t *testing.T) {
+ filter := Filter{
+ ErrorsOnly: true,
+ File: &StringFilter{Pattern: "access"},
+ FD: &NumericFilter{Op: OpEq, Value: 7},
+ }
+ clone := filter.Clone()
+ clone.File.Pattern = "different"
+ clone.FD.Value = 3
+
+ if filter.File.Pattern != "access" {
+ t.Fatalf("Clone() should deep-copy string filters")
+ }
+ if filter.FD.Value != 7 {
+ t.Fatalf("Clone() should deep-copy numeric filters")
+ }
+ if !filter.Matches(testCandidate()) {
+ t.Fatalf("errors-only filter should match error candidate")
+ }
+
+ candidate := testCandidate()
+ candidate.isError = false
+ if filter.Matches(candidate) {
+ t.Fatalf("errors-only filter should reject non-error candidate")
+ }
+}
+
+func TestFilterSummaryAndDurationParsing(t *testing.T) {
+ filter := Filter{
+ ErrorsOnly: true,
+ Syscall: &StringFilter{Pattern: "read"},
+ PID: &NumericFilter{Op: OpEq, Value: 1234},
+ LatencyNs: &NumericFilter{Op: OpGt, Value: 1_000_000},
+ }
+ got := filter.Summary()
+ for _, want := range []string{"errors", "syscall~read", "pid=1234", "latency>1ms"} {
+ if !strings.Contains(got, want) {
+ t.Fatalf("Summary() = %q, missing %q", got, want)
+ }
+ }
+
+ for _, tc := range []struct {
+ in string
+ want int64
+ }{
+ {in: "1ms", want: 1_000_000},
+ {in: "10us", want: 10_000},
+ {in: "500ns", want: 500},
+ {in: "42", want: 42},
+ } {
+ gotNs, err := ParseDurationNs(tc.in)
+ if err != nil {
+ t.Fatalf("ParseDurationNs(%q) err = %v", tc.in, err)
+ }
+ if gotNs != tc.want {
+ t.Fatalf("ParseDurationNs(%q) = %d, want %d", tc.in, gotNs, tc.want)
+ }
+ }
+ if _, err := ParseDurationNs("garbage"); err == nil {
+ t.Fatalf("ParseDurationNs(garbage) expected error")
+ }
+}
diff --git a/internal/tui/eventstream/filter.go b/internal/tui/eventstream/filter.go
index 4e0daf7..61d8c33 100644
--- a/internal/tui/eventstream/filter.go
+++ b/internal/tui/eventstream/filter.go
@@ -1,227 +1,21 @@
package eventstream
-import (
- "fmt"
- "strconv"
- "strings"
- "time"
-)
+import "ior/internal/globalfilter"
-type CompareOp int
+type CompareOp = globalfilter.CompareOp
+type NumericFilter = globalfilter.NumericFilter
+type StringFilter = globalfilter.StringFilter
+type Filter = globalfilter.Filter
const (
- OpEq CompareOp = iota
- OpNeq
- OpGt
- OpGte
- OpLt
- OpLte
+ OpEq = globalfilter.OpEq
+ OpNeq = globalfilter.OpNeq
+ OpGt = globalfilter.OpGt
+ OpGte = globalfilter.OpGte
+ OpLt = globalfilter.OpLt
+ OpLte = globalfilter.OpLte
)
-type NumericFilter struct {
- Op CompareOp
- Value int64
-}
-
-type StringFilter struct {
- Pattern string
-}
-
-type Filter struct {
- Syscall *StringFilter
- Comm *StringFilter
- File *StringFilter
- PID *NumericFilter
- TID *NumericFilter
- FD *NumericFilter
- LatencyNs *NumericFilter
- GapNs *NumericFilter
- Bytes *NumericFilter
- RetVal *NumericFilter
- ErrorsOnly bool
-}
-
-func (f Filter) Matches(ev *StreamEvent) bool {
- if ev == nil {
- return false
- }
- if f.ErrorsOnly && !ev.IsError {
- return false
- }
- if !matchString(f.Syscall, ev.Syscall) {
- return false
- }
- if !matchString(f.Comm, ev.Comm) {
- return false
- }
- if !matchString(f.File, ev.FileName) {
- return false
- }
- if !matchNumeric(f.PID, int64(ev.PID)) {
- return false
- }
- if !matchNumeric(f.TID, int64(ev.TID)) {
- return false
- }
- if !matchNumeric(f.FD, int64(ev.FD)) {
- return false
- }
- if !matchNumeric(f.LatencyNs, int64(ev.DurationNs)) {
- return false
- }
- if !matchNumeric(f.GapNs, int64(ev.GapNs)) {
- return false
- }
- if !matchNumeric(f.Bytes, int64(ev.Bytes)) {
- return false
- }
- if !matchNumeric(f.RetVal, ev.RetVal) {
- return false
- }
- return true
-}
-
-func (f Filter) IsActive() bool {
- if f.ErrorsOnly {
- return true
- }
- for _, sf := range []*StringFilter{f.Syscall, f.Comm, f.File} {
- if sf != nil && strings.TrimSpace(sf.Pattern) != "" {
- return true
- }
- }
- for _, nf := range []*NumericFilter{f.PID, f.TID, f.FD, f.LatencyNs, f.GapNs, f.Bytes, f.RetVal} {
- if nf != nil {
- return true
- }
- }
- return false
-}
-
-func (f Filter) Summary() string {
- parts := make([]string, 0, 10)
- if f.ErrorsOnly {
- parts = append(parts, "errors")
- }
- parts = appendStringSummary(parts, "syscall", f.Syscall)
- parts = appendStringSummary(parts, "comm", f.Comm)
- parts = appendStringSummary(parts, "file", f.File)
- parts = appendNumericSummary(parts, "pid", f.PID, false)
- parts = appendNumericSummary(parts, "tid", f.TID, false)
- parts = appendNumericSummary(parts, "fd", f.FD, false)
- parts = appendNumericSummary(parts, "latency", f.LatencyNs, true)
- parts = appendNumericSummary(parts, "gap", f.GapNs, true)
- parts = appendNumericSummary(parts, "bytes", f.Bytes, false)
- parts = appendNumericSummary(parts, "ret", f.RetVal, false)
- if len(parts) == 0 {
- return "all"
- }
- return strings.Join(parts, " ")
-}
-
func ParseDurationNs(input string) (int64, error) {
- s := strings.TrimSpace(strings.ToLower(input))
- if s == "" {
- return 0, fmt.Errorf("empty duration")
- }
- s = strings.ReplaceAll(s, "µs", "us")
- s = strings.ReplaceAll(s, "μs", "us")
- if onlyDigits(s) || strings.HasPrefix(s, "-") && onlyDigits(s[1:]) {
- v, err := strconv.ParseInt(s, 10, 64)
- if err != nil {
- return 0, err
- }
- return v, nil
- }
- d, err := time.ParseDuration(s)
- if err != nil {
- return 0, err
- }
- return d.Nanoseconds(), nil
-}
-
-func appendStringSummary(parts []string, name string, sf *StringFilter) []string {
- if sf == nil {
- return parts
- }
- pattern := strings.TrimSpace(sf.Pattern)
- if pattern == "" {
- return parts
- }
- return append(parts, fmt.Sprintf("%s~%s", name, pattern))
-}
-
-func appendNumericSummary(parts []string, name string, nf *NumericFilter, duration bool) []string {
- if nf == nil {
- return parts
- }
- value := strconv.FormatInt(nf.Value, 10)
- if duration {
- value = time.Duration(nf.Value).String()
- }
- return append(parts, fmt.Sprintf("%s%s%s", name, compareOpSymbol(nf.Op), value))
-}
-
-func matchString(sf *StringFilter, value string) bool {
- if sf == nil {
- return true
- }
- pattern := strings.ToLower(strings.TrimSpace(sf.Pattern))
- if pattern == "" {
- return true
- }
- return strings.Contains(strings.ToLower(value), pattern)
-}
-
-func matchNumeric(nf *NumericFilter, value int64) bool {
- if nf == nil {
- return true
- }
- switch nf.Op {
- case OpEq:
- return value == nf.Value
- case OpNeq:
- return value != nf.Value
- case OpGt:
- return value > nf.Value
- case OpGte:
- return value >= nf.Value
- case OpLt:
- return value < nf.Value
- case OpLte:
- return value <= nf.Value
- default:
- return false
- }
-}
-
-func compareOpSymbol(op CompareOp) string {
- switch op {
- case OpEq:
- return "="
- case OpNeq:
- return "!="
- case OpGt:
- return ">"
- case OpGte:
- return ">="
- case OpLt:
- return "<"
- case OpLte:
- return "<="
- default:
- return "?"
- }
-}
-
-func onlyDigits(s string) bool {
- if s == "" {
- return false
- }
- for _, ch := range s {
- if ch < '0' || ch > '9' {
- return false
- }
- }
- return true
+ return globalfilter.ParseDurationNs(input)
}
diff --git a/internal/tui/eventstream/model.go b/internal/tui/eventstream/model.go
index 12aff4d..ee65793 100644
--- a/internal/tui/eventstream/model.go
+++ b/internal/tui/eventstream/model.go
@@ -751,7 +751,7 @@ func (m *Model) applyFilterFromSelectedCell() bool {
}
ev := m.filtered[m.selectedIdx]
targetSeq := ev.Seq
- next := cloneFilter(m.filter)
+ next := m.filter.Clone()
action := ""
switch m.selectedCol {
@@ -789,7 +789,7 @@ func (m *Model) applyFilterFromSelectedCell() bool {
return false
}
- m.filterStack = append(m.filterStack, cloneFilter(m.filter))
+ m.filterStack = append(m.filterStack, m.filter.Clone())
m.filterActionStack = append(m.filterActionStack, action)
m.filter = next
m.applyFilter()
@@ -807,7 +807,7 @@ func (m *Model) popFilter() bool {
if len(m.filterActionStack) > 0 {
m.filterActionStack = m.filterActionStack[:len(m.filterActionStack)-1]
}
- m.filter = cloneFilter(last)
+ m.filter = last.Clone()
m.applyFilter()
m.restoreSelectionBySeq(targetSeq)
return true
@@ -833,37 +833,6 @@ func (m *Model) restoreSelectionBySeq(seq uint64) {
}
}
-func cloneFilter(in Filter) Filter {
- out := in
- out.Syscall = cloneStringFilter(in.Syscall)
- out.Comm = cloneStringFilter(in.Comm)
- out.File = cloneStringFilter(in.File)
- out.PID = cloneNumericFilter(in.PID)
- out.TID = cloneNumericFilter(in.TID)
- out.FD = cloneNumericFilter(in.FD)
- out.LatencyNs = cloneNumericFilter(in.LatencyNs)
- out.GapNs = cloneNumericFilter(in.GapNs)
- out.Bytes = cloneNumericFilter(in.Bytes)
- out.RetVal = cloneNumericFilter(in.RetVal)
- return out
-}
-
-func cloneStringFilter(in *StringFilter) *StringFilter {
- if in == nil {
- return nil
- }
- out := *in
- return &out
-}
-
-func cloneNumericFilter(in *NumericFilter) *NumericFilter {
- if in == nil {
- return nil
- }
- out := *in
- return &out
-}
-
func (m *Model) clampSelection() {
if len(m.filtered) == 0 {
m.selectedIdx = -1
diff --git a/internal/tui/eventstream/streamevent.go b/internal/tui/eventstream/streamevent.go
index 5f1e27f..9238a84 100644
--- a/internal/tui/eventstream/streamevent.go
+++ b/internal/tui/eventstream/streamevent.go
@@ -23,6 +23,50 @@ type StreamEvent struct {
FD int32
}
+func (e StreamEvent) SyscallValue() string {
+ return e.Syscall
+}
+
+func (e StreamEvent) CommValue() string {
+ return e.Comm
+}
+
+func (e StreamEvent) FileValue() string {
+ return e.FileName
+}
+
+func (e StreamEvent) PIDValue() uint32 {
+ return e.PID
+}
+
+func (e StreamEvent) TIDValue() uint32 {
+ return e.TID
+}
+
+func (e StreamEvent) FDValue() int32 {
+ return e.FD
+}
+
+func (e StreamEvent) LatencyValue() uint64 {
+ return e.DurationNs
+}
+
+func (e StreamEvent) GapValue() uint64 {
+ return e.GapNs
+}
+
+func (e StreamEvent) BytesValue() uint64 {
+ return e.Bytes
+}
+
+func (e StreamEvent) ReturnValue() int64 {
+ return e.RetVal
+}
+
+func (e StreamEvent) ErrorValue() bool {
+ return e.IsError
+}
+
// UnknownFD marks events that are not associated with a file descriptor.
const UnknownFD int32 = -1