diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/flags/flags.go | 28 | ||||
| -rw-r--r-- | internal/flags/flags_test.go | 99 | ||||
| -rw-r--r-- | internal/generate/bpfhandler.go | 4 | ||||
| -rw-r--r-- | internal/generate/classify.go | 55 | ||||
| -rw-r--r-- | internal/generate/tracepointsgo.go | 107 | ||||
| -rw-r--r-- | internal/generate/tracepointsgo_test.go | 22 | ||||
| -rw-r--r-- | internal/probemanager/manager_test.go | 41 | ||||
| -rw-r--r-- | internal/tracepoints/dimension_selector.go | 219 | ||||
| -rw-r--r-- | internal/tracepoints/dimension_selector_test.go | 147 | ||||
| -rw-r--r-- | internal/tracepoints/generated_tracepoints.go | 740 | ||||
| -rw-r--r-- | internal/tracepoints/selector.go | 46 | ||||
| -rw-r--r-- | internal/tracepoints/selector_test.go | 14 |
12 files changed, 1507 insertions, 15 deletions
diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 3a6456a..547ba8e 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -148,12 +148,12 @@ func Parse() (Config, error) { // fresh FlagSet and custom argument slices without touching global state. func parseFromFlagSet(fs *flag.FlagSet, args []string) (Config, error) { cfg := NewFlags() - tpsAttach, tpsExclude, fields, familySampling, syscallSampling := registerFlags(fs, &cfg) + tpsAttach, tpsExclude, fields, familySampling, syscallSampling, dims := registerFlags(fs, &cfg) if err := fs.Parse(args); err != nil { return Config{}, err } - if err := resolvePostParseFields(&cfg, tpsAttach, tpsExclude, fields); err != nil { + if err := resolvePostParseFields(&cfg, tpsAttach, tpsExclude, fields, dims); err != nil { return Config{}, err } if err := resolveSamplingRates(&cfg, familySampling, syscallSampling); err != nil { @@ -167,9 +167,10 @@ func parseFromFlagSet(fs *flag.FlagSet, args []string) (Config, error) { // registerFlags binds all CLI flags to cfg and returns the string pointers for // fields that require post-parse resolution (tracepoint regexes, collapse fields). -func registerFlags(fs *flag.FlagSet, cfg *Config) (tpsAttach, tpsExclude, fields, familySampling, syscallSampling *string) { +func registerFlags(fs *flag.FlagSet, cfg *Config) (tpsAttach, tpsExclude, fields, familySampling, syscallSampling *string, dims *tracepoints.DimensionSelectorConfig) { validFields := collapse.ValidFields() validCounts := collapse.ValidCountFields() + dimensionCfg := &tracepoints.DimensionSelectorConfig{} fs.IntVar(&cfg.PidFilter, "pid", cfg.PidFilter, "Filter for processes ID") fs.IntVar(&cfg.TidFilter, "tid", cfg.TidFilter, "Filter for thread ID") @@ -182,6 +183,18 @@ func registerFlags(fs *flag.FlagSet, cfg *Config) (tpsAttach, tpsExclude, fields tpsAttach = fs.String("tps", "", "Comma separated list regexes for tracepoints to load") tpsExclude = fs.String("tpsExclude", "", "Comma separated list regexes for tracepoints to exclude") + fs.StringVar(&dimensionCfg.TraceFamilies, "trace-families", "", + "Comma separated syscall families to attach (for example FS,Time,Network)") + fs.StringVar(&dimensionCfg.TraceKinds, "trace-kinds", "", + "Comma separated tracepoint kinds to attach (for example fd,open,sleep,epoll-ctl)") + fs.StringVar(&dimensionCfg.TraceSyscalls, "trace-syscalls", "", + "Comma separated syscall names to attach (for example openat,read,nanosleep)") + fs.StringVar(&dimensionCfg.NoTraceFamilies, "no-trace-families", "", + "Comma separated syscall families to exclude from attachment") + fs.StringVar(&dimensionCfg.NoTraceKinds, "no-trace-kinds", "", + "Comma separated tracepoint kinds to exclude from attachment") + fs.StringVar(&dimensionCfg.NoTraceSyscalls, "no-trace-syscalls", "", + "Comma separated syscall names to exclude from attachment") fs.BoolVar(&cfg.PlainMode, "plain", false, "Enable plain CSV output mode (disable TUI)") fs.BoolVar(&cfg.FlamegraphOutput, "flamegraph", false, "Write aggregated .ior.zst output for trace/integration workflows") @@ -204,15 +217,18 @@ func registerFlags(fs *flag.FlagSet, cfg *Config) (tpsAttach, tpsExclude, fields fmt.Sprintf("Comma separated list of fields to collapse, valid are: %v", validFields)) fs.StringVar(&cfg.CountField, "count", cfg.CountField, fmt.Sprintf("Count field to collapse, valid are: %v", validCounts)) - return tpsAttach, tpsExclude, fields, familySampling, syscallSampling + return tpsAttach, tpsExclude, fields, familySampling, syscallSampling, dimensionCfg } // resolvePostParseFields compiles the tracepoint selector and collapse field // list from the raw string flags that cannot be bound directly to cfg fields. -func resolvePostParseFields(cfg *Config, tpsAttach, tpsExclude, fields *string) error { +func resolvePostParseFields(cfg *Config, tpsAttach, tpsExclude, fields *string, dims *tracepoints.DimensionSelectorConfig) error { // Parse the tracepoint include/exclude regex lists into a Selector. // The Selector owns all matching logic; Config is purely a data carrier. - sel, err := tracepoints.ParseSelector(*tpsAttach, *tpsExclude) + if dims == nil { + dims = &tracepoints.DimensionSelectorConfig{} + } + sel, err := tracepoints.ParseSelectorWithDimensions(*tpsAttach, *tpsExclude, *dims) if err != nil { return err } diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index 1feeafd..93df7b0 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -152,6 +152,105 @@ func TestParseInvalidTracepointRegexReturnsError(t *testing.T) { } } +func TestParseDefaultTraceDimensionsFSOnly(t *testing.T) { + cfg, err := parseForTest(t) + if err != nil { + t.Fatalf("parse returned error: %v", err) + } + if !cfg.TracepointSelector.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat attached by default") + } + if cfg.TracepointSelector.ShouldAttach("sys_enter_nanosleep") { + t.Fatal("expected nanosleep excluded by default") + } +} + +func TestParseTraceFamiliesFlag(t *testing.T) { + cfg, err := parseForTest(t, "-trace-families", "Time") + if err != nil { + t.Fatalf("parse returned error: %v", err) + } + if !cfg.TracepointSelector.ShouldAttach("sys_enter_nanosleep") { + t.Fatal("expected nanosleep attached for Time family") + } + if cfg.TracepointSelector.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat excluded when only Time family enabled") + } +} + +func TestParseTraceKindsFlag(t *testing.T) { + cfg, err := parseForTest(t, "-trace-kinds", "sleep") + if err != nil { + t.Fatalf("parse returned error: %v", err) + } + if !cfg.TracepointSelector.ShouldAttach("sys_enter_nanosleep") { + t.Fatal("expected nanosleep attached for sleep kind") + } + if cfg.TracepointSelector.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat excluded for sleep-only selector") + } +} + +func TestParseTraceSyscallsFlag(t *testing.T) { + cfg, err := parseForTest(t, "-trace-syscalls", "openat") + if err != nil { + t.Fatalf("parse returned error: %v", err) + } + if !cfg.TracepointSelector.ShouldAttach("sys_enter_openat") || !cfg.TracepointSelector.ShouldAttach("sys_exit_openat") { + t.Fatal("expected openat enter/exit attached") + } + if cfg.TracepointSelector.ShouldAttach("sys_enter_write") { + t.Fatal("expected write excluded when only openat enabled") + } +} + +func TestParseTraceDimensionsUnionAndExclusions(t *testing.T) { + cfg, err := parseForTest(t, + "-trace-families", "Time", + "-trace-syscalls", "openat", + "-no-trace-syscalls", "openat", + ) + if err != nil { + t.Fatalf("parse returned error: %v", err) + } + if cfg.TracepointSelector.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat excluded by no-trace-syscalls") + } + if !cfg.TracepointSelector.ShouldAttach("sys_enter_nanosleep") { + t.Fatal("expected nanosleep still attached from trace-families") + } +} + +func TestParseTraceFamiliesRejectsUnknown(t *testing.T) { + _, err := parseForTest(t, "-trace-families", "Nope") + if err == nil { + t.Fatal("expected parse error") + } + if !strings.Contains(err.Error(), "invalid syscall family") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestParseTraceKindsRejectsUnknown(t *testing.T) { + _, err := parseForTest(t, "-trace-kinds", "not-a-kind") + if err == nil { + t.Fatal("expected parse error") + } + if !strings.Contains(err.Error(), "invalid syscall kind") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestParseTraceSyscallsRejectsUnknown(t *testing.T) { + _, err := parseForTest(t, "-trace-syscalls", "definitely_not_syscall") + if err == nil { + t.Fatal("expected parse error") + } + if !strings.Contains(err.Error(), "invalid syscall in trace selector") { + t.Fatalf("unexpected error: %v", err) + } +} + func TestParseResetTimerDefault(t *testing.T) { cfg, err := parseForTest(t) if err != nil { diff --git a/internal/generate/bpfhandler.go b/internal/generate/bpfhandler.go index 9365c52..dce776a 100644 --- a/internal/generate/bpfhandler.go +++ b/internal/generate/bpfhandler.go @@ -23,9 +23,9 @@ func generateBPFHandler(tp GeneratedTracepoint) string { } eventStruct := eventStructName(tp.Classification.Kind) - comment := eventStruct + comment := fmt.Sprintf("%s (kind=%s)", eventStruct, tp.Classification.Kind.MetadataName()) if tp.Classification.Kind == KindRet { - comment = fmt.Sprintf("%s (%s)", eventStruct, ClassifyRet(f.Name)) + comment = fmt.Sprintf("%s (%s) (kind=%s)", eventStruct, ClassifyRet(f.Name), tp.Classification.Kind.MetadataName()) } eventTypeConst := eventTypeConstant(tp.Classification.Kind, isEnter) diff --git a/internal/generate/classify.go b/internal/generate/classify.go index af7d78d..9e9df9c 100644 --- a/internal/generate/classify.go +++ b/internal/generate/classify.go @@ -32,6 +32,61 @@ const ( KindPerfOpen ) +func (k TracepointKind) MetadataName() string { + switch k { + case KindFd: + return "fd" + case KindOpen: + return "open" + case KindMqOpen: + return "mq-open" + case KindExec: + return "exec" + case KindPathname: + return "pathname" + case KindName: + return "name" + case KindRet: + return "ret" + case KindFcntl: + return "fcntl" + case KindNull: + return "null" + case KindDup3: + return "dup3" + case KindOpenByHandleAt: + return "open-by-handle-at" + case KindSocket: + return "socket" + case KindSocketpair: + return "socketpair" + case KindAccept: + return "accept" + case KindPipe: + return "pipe" + case KindEventfd: + return "eventfd" + case KindEpollCtl: + return "epoll-ctl" + case KindTwoFd: + return "two-fd" + case KindPoll: + return "poll" + case KindMem: + return "mem" + case KindSleep: + return "sleep" + case KindKeyctl: + return "keyctl" + case KindPtrace: + return "ptrace" + case KindPerfOpen: + return "perf-open" + default: + return "none" + } +} + type RetClassification string const ( diff --git a/internal/generate/tracepointsgo.go b/internal/generate/tracepointsgo.go index 0542f64..9d92047 100644 --- a/internal/generate/tracepointsgo.go +++ b/internal/generate/tracepointsgo.go @@ -5,14 +5,35 @@ import ( "fmt" "io" "regexp" + "sort" "strings" ) var secRe = regexp.MustCompile(`^SEC.*sys_((?:enter|exit)_[a-z_0-9]+)`) +var kindLineRe = regexp.MustCompile(`^(sys_enter_[a-z0-9_]+)\s+is a struct\s+([a-z0-9_]+)\s*$`) // ExtractTracepoints reads generated C code and extracts tracepoint names from // SEC annotations, producing the generated_tracepoints.go content. func ExtractTracepoints(r io.Reader) (string, error) { + return ExtractTracepointsWithKinds(r, strings.NewReader("")) +} + +// ExtractTracepointsWithKinds reads generated C code and the +// generated_tracepoints_result.txt metadata to produce +// internal/tracepoints/generated_tracepoints.go. +func ExtractTracepointsWithKinds(cReader io.Reader, kindsReader io.Reader) (string, error) { + tracepoints, err := extractTracepoints(cReader) + if err != nil { + return "", err + } + syscallKinds, err := extractSyscallKinds(kindsReader) + if err != nil { + return "", err + } + return formatTracepointsGo(tracepoints, syscallKinds), nil +} + +func extractTracepoints(r io.Reader) ([]string, error) { scanner := bufio.NewScanner(r) var tracepoints []string @@ -24,13 +45,58 @@ func ExtractTracepoints(r io.Reader) (string, error) { } if err := scanner.Err(); err != nil { - return "", fmt.Errorf("scanning input: %w", err) + return nil, fmt.Errorf("scanning input: %w", err) } + return tracepoints, nil +} - return formatTracepointsGo(tracepoints), nil +func extractSyscallKinds(r io.Reader) (map[string]string, error) { + kinds := make(map[string]string) + if r == nil { + return kinds, nil + } + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + continue + } + m := kindLineRe.FindStringSubmatch(line) + if m == nil { + continue + } + syscall := strings.TrimPrefix(m[1], "sys_enter_") + kind := normalizeStructKind(m[2]) + if kind == "" { + continue + } + kinds[syscall] = kind + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("scan kind metadata: %w", err) + } + return kinds, nil +} + +func normalizeStructKind(structName string) string { + base := strings.TrimSuffix(structName, "_event") + switch base { + case "mq_open": + return "mq-open" + case "open_by_handle_at": + return "open-by-handle-at" + case "two_fd": + return "two-fd" + case "epoll_ctl": + return "epoll-ctl" + case "perf_open": + return "perf-open" + default: + return base + } } -func formatTracepointsGo(tracepoints []string) string { +func formatTracepointsGo(tracepoints []string, syscallKinds map[string]string) string { var b strings.Builder b.WriteString("// Code generated - don't change manually!\n") b.WriteString("package tracepoints\n\n") @@ -39,5 +105,40 @@ func formatTracepointsGo(tracepoints []string) string { fmt.Fprintf(&b, "\t%q,\n", tp) } b.WriteString("}\n") + b.WriteString("\n") + b.WriteString("var syscallFamilies = map[string]string{\n") + syscallFamilies := make(map[string]string) + for _, tp := range tracepoints { + syscall := strings.TrimPrefix(tp, "sys_enter_") + syscall = strings.TrimPrefix(syscall, "sys_exit_") + if syscall == "" { + continue + } + if _, ok := syscallFamilies[syscall]; ok { + continue + } + family := ClassifySyscallFamily("sys_enter_" + syscall) + syscallFamilies[syscall] = string(family) + } + familySyscalls := make([]string, 0, len(syscallFamilies)) + for syscall := range syscallFamilies { + familySyscalls = append(familySyscalls, syscall) + } + sort.Strings(familySyscalls) + for _, syscall := range familySyscalls { + fmt.Fprintf(&b, "\t%q: %q,\n", syscall, syscallFamilies[syscall]) + } + b.WriteString("}\n") + b.WriteString("\n") + b.WriteString("var syscallKinds = map[string]string{\n") + syscalls := make([]string, 0, len(syscallKinds)) + for syscall := range syscallKinds { + syscalls = append(syscalls, syscall) + } + sort.Strings(syscalls) + for _, syscall := range syscalls { + fmt.Fprintf(&b, "\t%q: %q,\n", syscall, syscallKinds[syscall]) + } + b.WriteString("}\n") return b.String() } diff --git a/internal/generate/tracepointsgo_test.go b/internal/generate/tracepointsgo_test.go index 978633b..ebad63d 100644 --- a/internal/generate/tracepointsgo_test.go +++ b/internal/generate/tracepointsgo_test.go @@ -51,6 +51,8 @@ func TestExtractTracepoints(t *testing.T) { requireContains(t, output, `"sys_enter_close",`) requireContains(t, output, `"sys_exit_close",`) requireContains(t, output, "var List = []string{") + requireContains(t, output, "var syscallFamilies = map[string]string{") + requireContains(t, output, "var syscallKinds = map[string]string{") // Should NOT contain ignore comments or defines if strings.Contains(output, "kill") { @@ -78,6 +80,8 @@ func TestExtractTracepointsEmpty(t *testing.T) { t.Fatal(err) } requireContains(t, output, "var List = []string{") + requireContains(t, output, "var syscallFamilies = map[string]string{") + requireContains(t, output, "var syscallKinds = map[string]string{") requireContains(t, output, "}") } @@ -115,7 +119,25 @@ func TestExtractTracepointsNoSECLines(t *testing.T) { t.Fatalf("unexpected error: %v", err) } requireContains(t, output, "var List = []string{") + requireContains(t, output, "var syscallFamilies = map[string]string{") + requireContains(t, output, "var syscallKinds = map[string]string{") if strings.Contains(output, `"sys_`) { t.Error("input with no SEC lines should produce empty list") } } + +func TestExtractTracepointsWithKinds(t *testing.T) { + kindData := `sys_enter_read is a struct fd_event +sys_enter_open_by_handle_at is a struct open_by_handle_at_event +sys_enter_mq_open is a struct mq_open_event +sys_enter_epoll_ctl is a struct epoll_ctl_event +` + output, err := ExtractTracepointsWithKinds(strings.NewReader(sampleGeneratedC), strings.NewReader(kindData)) + if err != nil { + t.Fatalf("ExtractTracepointsWithKinds failed: %v", err) + } + requireContains(t, output, `"read": "fd",`) + requireContains(t, output, `"open_by_handle_at": "open-by-handle-at",`) + requireContains(t, output, `"mq_open": "mq-open",`) + requireContains(t, output, `"epoll_ctl": "epoll-ctl",`) +} diff --git a/internal/probemanager/manager_test.go b/internal/probemanager/manager_test.go index b75a579..ee436b9 100644 --- a/internal/probemanager/manager_test.go +++ b/internal/probemanager/manager_test.go @@ -2,6 +2,7 @@ package probemanager import ( "errors" + "ior/internal/tracepoints" "strings" "sync" "testing" @@ -127,6 +128,46 @@ func TestManagerAttachAllToggleAndCounts(t *testing.T) { } } +func TestManagerAttachAllWithDimensionSelectorAttachesOnlyEnabledSyscalls(t *testing.T) { + attacher := &fakeAttacher{ + programs: map[string]*fakeProgram{ + "handle_sys_enter_openat": {}, + "handle_sys_exit_openat": {}, + "handle_sys_enter_write": {}, + "handle_sys_exit_write": {}, + }, + errs: map[string]error{}, + } + mgr := NewManager(attacher) + selector, err := tracepoints.ParseSelectorWithDimensions("", "", tracepoints.DimensionSelectorConfig{ + TraceSyscalls: "openat", + }) + if err != nil { + t.Fatalf("build selector: %v", err) + } + + err = mgr.AttachAll(selector.ShouldAttach, []string{ + "sys_enter_openat", "sys_exit_openat", + "sys_enter_write", "sys_exit_write", + }, nil) + if err != nil { + t.Fatalf("AttachAll returned error: %v", err) + } + + if got := attacher.programs["handle_sys_enter_openat"].attachCalls(); got != 1 { + t.Fatalf("openat enter attach calls = %d, want 1", got) + } + if got := attacher.programs["handle_sys_exit_openat"].attachCalls(); got != 1 { + t.Fatalf("openat exit attach calls = %d, want 1", got) + } + if got := attacher.programs["handle_sys_enter_write"].attachCalls(); got != 0 { + t.Fatalf("write enter attach calls = %d, want 0", got) + } + if got := attacher.programs["handle_sys_exit_write"].attachCalls(); got != 0 { + t.Fatalf("write exit attach calls = %d, want 0", got) + } +} + func TestManagerAttachSerializesConcurrentCalls(t *testing.T) { enterBlocked := make(chan struct{}) releaseEnter := make(chan struct{}) diff --git a/internal/tracepoints/dimension_selector.go b/internal/tracepoints/dimension_selector.go new file mode 100644 index 0000000..22a70fb --- /dev/null +++ b/internal/tracepoints/dimension_selector.go @@ -0,0 +1,219 @@ +package tracepoints + +import ( + "fmt" + "sort" + "strings" + + "ior/internal/types" +) + +// DimensionSelectorConfig holds attach-time syscall-dimension selection inputs. +// Each field accepts comma-separated values. +type DimensionSelectorConfig struct { + TraceFamilies string + TraceKinds string + TraceSyscalls string + NoTraceFamilies string + NoTraceKinds string + NoTraceSyscalls string +} + +// ParseSelectorWithDimensions compiles regex-based attach/exclude filters and +// applies attach-time syscall dimension gating. +func ParseSelectorWithDimensions(attach, exclude string, dims DimensionSelectorConfig) (Selector, error) { + sel, err := ParseSelector(attach, exclude) + if err != nil { + return Selector{}, err + } + + allow, err := buildAllowedSyscalls(dims) + if err != nil { + return Selector{}, err + } + sel.RestrictSyscalls = true + sel.Syscalls = allow + return sel, nil +} + +func buildAllowedSyscalls(dims DimensionSelectorConfig) (map[string]struct{}, error) { + knownSyscalls := allKnownSyscalls() + knownKinds := allKnownKinds() + + includeFamilies, familyFilterProvided, err := parseFamiliesCSV(dims.TraceFamilies) + if err != nil { + return nil, err + } + includeKinds, kindFilterProvided, err := parseKindsCSV(dims.TraceKinds, knownKinds) + if err != nil { + return nil, err + } + includeSyscalls, syscallFilterProvided, err := parseSyscallsCSV(dims.TraceSyscalls, knownSyscalls) + if err != nil { + return nil, err + } + + allow := make(map[string]struct{}) + hasPositive := familyFilterProvided || kindFilterProvided || syscallFilterProvided + if hasPositive { + for syscall, family := range syscallFamilies { + if _, ok := includeFamilies[family]; ok { + allow[syscall] = struct{}{} + } + } + for syscall, kind := range syscallKinds { + if _, ok := includeKinds[kind]; ok { + allow[syscall] = struct{}{} + } + } + for syscall := range includeSyscalls { + allow[syscall] = struct{}{} + } + } else { + // Backward compatibility default: keep existing file-I/O coverage on and + // leave newly-expanded non-IO families disabled unless explicitly opted in. + for syscall, family := range syscallFamilies { + if family == string(types.FamilyFS) { + allow[syscall] = struct{}{} + } + } + } + + excludeFamilies, _, err := parseFamiliesCSV(dims.NoTraceFamilies) + if err != nil { + return nil, err + } + excludeKinds, _, err := parseKindsCSV(dims.NoTraceKinds, knownKinds) + if err != nil { + return nil, err + } + excludeSyscalls, _, err := parseSyscallsCSV(dims.NoTraceSyscalls, knownSyscalls) + if err != nil { + return nil, err + } + + for syscall := range allow { + if _, ok := excludeSyscalls[syscall]; ok { + delete(allow, syscall) + continue + } + if family, ok := syscallFamilies[syscall]; ok { + if _, excluded := excludeFamilies[family]; excluded { + delete(allow, syscall) + continue + } + } + if kind, ok := syscallKinds[syscall]; ok { + if _, excluded := excludeKinds[kind]; excluded { + delete(allow, syscall) + } + } + } + + return allow, nil +} + +func parseFamiliesCSV(raw string) (map[string]struct{}, bool, error) { + values, provided := splitCSV(raw) + if !provided { + return map[string]struct{}{}, false, nil + } + out := make(map[string]struct{}, len(values)) + for _, value := range values { + family, ok := types.ParseSyscallFamily(value) + if !ok { + return nil, false, fmt.Errorf("invalid syscall family in trace selector: %q", value) + } + out[string(family)] = struct{}{} + } + return out, true, nil +} + +func parseKindsCSV(raw string, knownKinds map[string]struct{}) (map[string]struct{}, bool, error) { + values, provided := splitCSV(raw) + if !provided { + return map[string]struct{}{}, false, nil + } + out := make(map[string]struct{}, len(values)) + for _, value := range values { + kind := normalizeKind(value) + if _, ok := knownKinds[kind]; !ok { + return nil, false, fmt.Errorf("invalid syscall kind in trace selector: %q", value) + } + out[kind] = struct{}{} + } + return out, true, nil +} + +func parseSyscallsCSV(raw string, knownSyscalls map[string]struct{}) (map[string]struct{}, bool, error) { + values, provided := splitCSV(raw) + if !provided { + return map[string]struct{}{}, false, nil + } + out := make(map[string]struct{}, len(values)) + for _, value := range values { + syscall := strings.ToLower(strings.TrimSpace(value)) + if _, ok := knownSyscalls[syscall]; !ok { + return nil, false, fmt.Errorf("invalid syscall in trace selector: %q", value) + } + out[syscall] = struct{}{} + } + return out, true, nil +} + +func splitCSV(raw string) ([]string, bool) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, false + } + parts := strings.Split(raw, ",") + values := make([]string, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + values = append(values, part) + } + if len(values) == 0 { + return nil, false + } + return values, true +} + +func normalizeKind(raw string) string { + normalized := strings.ToLower(strings.TrimSpace(raw)) + normalized = strings.ReplaceAll(normalized, "_", "-") + return normalized +} + +func allKnownSyscalls() map[string]struct{} { + out := make(map[string]struct{}, len(syscallFamilies)) + for syscall := range syscallFamilies { + out[syscall] = struct{}{} + } + return out +} + +func allKnownKinds() map[string]struct{} { + out := make(map[string]struct{}) + for _, kind := range syscallKinds { + if kind == "" { + continue + } + out[kind] = struct{}{} + } + return out +} + +// KnownKinds returns the sorted, normalized attach-time kind names accepted by +// -trace-kinds and -no-trace-kinds. +func KnownKinds() []string { + kindSet := allKnownKinds() + kinds := make([]string, 0, len(kindSet)) + for kind := range kindSet { + kinds = append(kinds, kind) + } + sort.Strings(kinds) + return kinds +} diff --git a/internal/tracepoints/dimension_selector_test.go b/internal/tracepoints/dimension_selector_test.go new file mode 100644 index 0000000..cd7b0f8 --- /dev/null +++ b/internal/tracepoints/dimension_selector_test.go @@ -0,0 +1,147 @@ +package tracepoints + +import ( + "strings" + "testing" +) + +func TestParseSelectorWithDimensionsDefaultFSOnly(t *testing.T) { + sel, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !sel.ShouldAttach("sys_enter_openat") { + t.Fatal("expected FS syscall openat to be attached by default") + } + if sel.ShouldAttach("sys_enter_nanosleep") { + t.Fatal("expected non-FS syscall nanosleep to be excluded by default") + } +} + +func TestParseSelectorWithDimensionsFamilyOnly(t *testing.T) { + sel, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{ + TraceFamilies: "Time", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !sel.ShouldAttach("sys_enter_nanosleep") { + t.Fatal("expected nanosleep to be attached when Time family is enabled") + } + if sel.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat to be excluded when only Time family is enabled") + } +} + +func TestParseSelectorWithDimensionsKindOnly(t *testing.T) { + sel, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{ + TraceKinds: "sleep", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !sel.ShouldAttach("sys_enter_nanosleep") { + t.Fatal("expected nanosleep to be attached for sleep kind") + } + if sel.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat to be excluded when only sleep kind is enabled") + } +} + +func TestParseSelectorWithDimensionsSyscallOnly(t *testing.T) { + sel, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{ + TraceSyscalls: "openat", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !sel.ShouldAttach("sys_enter_openat") || !sel.ShouldAttach("sys_exit_openat") { + t.Fatal("expected both openat enter/exit tracepoints to be attached") + } + if sel.ShouldAttach("sys_enter_write") { + t.Fatal("expected write to be excluded when only openat is selected") + } +} + +func TestParseSelectorWithDimensionsUnionSemantics(t *testing.T) { + sel, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{ + TraceFamilies: "Time", + TraceSyscalls: "openat", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !sel.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat from syscall selector") + } + if !sel.ShouldAttach("sys_enter_nanosleep") { + t.Fatal("expected nanosleep from family selector") + } +} + +func TestParseSelectorWithDimensionsExclusionsOverridePositives(t *testing.T) { + sel, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{ + TraceFamilies: "FS", + NoTraceSyscalls: "openat", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if sel.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat to be excluded by -no-trace-syscalls") + } + if !sel.ShouldAttach("sys_enter_read") { + t.Fatal("expected other FS syscall (read) to remain attached") + } +} + +func TestParseSelectorWithDimensionsRegexStillApplies(t *testing.T) { + sel, err := ParseSelectorWithDimensions("^sys_enter_openat$,^sys_exit_openat$", "", DimensionSelectorConfig{ + TraceFamilies: "FS", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !sel.ShouldAttach("sys_enter_openat") { + t.Fatal("expected openat to pass regex+dimension filters") + } + if sel.ShouldAttach("sys_enter_read") { + t.Fatal("expected read to fail regex attach filters") + } +} + +func TestParseSelectorWithDimensionsRejectsInvalidFamily(t *testing.T) { + _, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{ + TraceFamilies: "Nope", + }) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "invalid syscall family") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestParseSelectorWithDimensionsRejectsInvalidKind(t *testing.T) { + _, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{ + TraceKinds: "not-a-kind", + }) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "invalid syscall kind") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestParseSelectorWithDimensionsRejectsInvalidSyscall(t *testing.T) { + _, err := ParseSelectorWithDimensions("", "", DimensionSelectorConfig{ + TraceSyscalls: "not_a_syscall", + }) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "invalid syscall in trace selector") { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/internal/tracepoints/generated_tracepoints.go b/internal/tracepoints/generated_tracepoints.go index 8277cbf..11913c8 100644 --- a/internal/tracepoints/generated_tracepoints.go +++ b/internal/tracepoints/generated_tracepoints.go @@ -737,3 +737,743 @@ var List = []string{ "sys_enter_rt_sigreturn", "sys_exit_rt_sigreturn", } + +var syscallFamilies = map[string]string{ + "accept": "Network", + "accept4": "Network", + "access": "FS", + "acct": "Misc", + "add_key": "Security", + "adjtimex": "Misc", + "alarm": "Misc", + "arch_prctl": "Process", + "bind": "Network", + "bpf": "Security", + "brk": "Memory", + "cachestat": "FS", + "capget": "Security", + "capset": "Security", + "chdir": "FS", + "chmod": "FS", + "chown": "FS", + "chroot": "FS", + "clock_adjtime": "Time", + "clock_getres": "Time", + "clock_gettime": "Time", + "clock_nanosleep": "Time", + "clock_settime": "Time", + "clone": "Process", + "clone3": "Process", + "close": "FS", + "close_range": "FS", + "connect": "Network", + "copy_file_range": "FS", + "creat": "FS", + "delete_module": "Security", + "dup": "FS", + "dup2": "FS", + "dup3": "FS", + "epoll_create": "Polling", + "epoll_create1": "Polling", + "epoll_ctl": "Polling", + "epoll_pwait": "Polling", + "epoll_pwait2": "Polling", + "epoll_wait": "Polling", + "eventfd": "IPC", + "eventfd2": "IPC", + "execve": "Process", + "execveat": "Process", + "exit": "Process", + "exit_group": "Process", + "faccessat": "FS", + "faccessat2": "FS", + "fadvise64": "FS", + "fallocate": "FS", + "fanotify_init": "Misc", + "fanotify_mark": "Misc", + "fchdir": "FS", + "fchmod": "FS", + "fchmodat": "FS", + "fchmodat2": "FS", + "fchown": "FS", + "fchownat": "FS", + "fcntl": "FS", + "fdatasync": "FS", + "fgetxattr": "FS", + "file_getattr": "Misc", + "file_setattr": "Misc", + "finit_module": "Security", + "flistxattr": "FS", + "flock": "FS", + "fork": "Process", + "fremovexattr": "FS", + "fsconfig": "FS", + "fsetxattr": "FS", + "fsmount": "FS", + "fsopen": "FS", + "fspick": "FS", + "fstatfs": "FS", + "fsync": "FS", + "ftruncate": "FS", + "futex": "Misc", + "futex_requeue": "Misc", + "futex_wait": "Misc", + "futex_waitv": "Misc", + "futex_wake": "Misc", + "futimesat": "FS", + "ge |
