summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-21 08:16:08 +0300
committerPaul Buetow <paul@buetow.org>2026-05-21 08:16:08 +0300
commitbe8735fe701f7398c19c17c394f4827614eab875 (patch)
treeaba59890563edb6e03f2eb82fee5d89b49fa2c81 /internal
parent3a5706f21d30258577a5934efb93c400dad723db (diff)
p7 add attach-time trace dimension gating
Diffstat (limited to 'internal')
-rw-r--r--internal/flags/flags.go28
-rw-r--r--internal/flags/flags_test.go99
-rw-r--r--internal/generate/bpfhandler.go4
-rw-r--r--internal/generate/classify.go55
-rw-r--r--internal/generate/tracepointsgo.go107
-rw-r--r--internal/generate/tracepointsgo_test.go22
-rw-r--r--internal/probemanager/manager_test.go41
-rw-r--r--internal/tracepoints/dimension_selector.go219
-rw-r--r--internal/tracepoints/dimension_selector_test.go147
-rw-r--r--internal/tracepoints/generated_tracepoints.go740
-rw-r--r--internal/tracepoints/selector.go46
-rw-r--r--internal/tracepoints/selector_test.go14
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",