diff options
| -rw-r--r-- | Magefile.go | 2 | ||||
| -rw-r--r-- | docs/parquet-querying.md | 6 | ||||
| -rw-r--r-- | integrationtests/rename_test.go | 48 | ||||
| -rw-r--r-- | internal/event/pair.go | 7 | ||||
| -rw-r--r-- | internal/eventloop_exit.go | 5 | ||||
| -rw-r--r-- | internal/parquet/schema.go | 7 | ||||
| -rw-r--r-- | internal/streamrow/row.go | 7 |
7 files changed, 78 insertions, 4 deletions
diff --git a/Magefile.go b/Magefile.go index 0396a9e..a708fb7 100644 --- a/Magefile.go +++ b/Magefile.go @@ -1170,7 +1170,7 @@ var expectedParquetColumns = []string{ "seq", "time_ns", "gap_ns", "latency_ns", "comm", "pid", "tid", "syscall", "family", "fd", "ret", "bytes", "address_space_bytes", "requested_sleep_ns", - "file", "is_error", "filter_epoch", + "file", "old_file", "is_error", "filter_epoch", "epoll_op", "epoll_target_fd", "epoll_events", } diff --git a/docs/parquet-querying.md b/docs/parquet-querying.md index 2ebf16e..b47ac8e 100644 --- a/docs/parquet-querying.md +++ b/docs/parquet-querying.md @@ -32,7 +32,8 @@ state, no installation needed beyond Docker. | `bytes` | UInt64 | Bytes transferred (0 if not applicable) | | `address_space_bytes` | UInt64 | Memory-region extent for memory syscalls (e.g. `munmap`/`mremap`); 0 otherwise | | `requested_sleep_ns` | Int64 | Requested sleep duration for nanosleep-style syscalls; 0 otherwise | -| `file` | String | File path (empty if not resolved) | +| `file` | String | File path (empty if not resolved); for rename/link syscalls this is the "new" path | +| `old_file` | String | Source/old path for rename-family (`rename`/`renameat`/`renameat2`) and link-family (`link`/`linkat`/`symlink`/`symlinkat`) syscalls; empty for other syscalls | | `is_error` | Bool | True when `ret` is a negative errno | | `filter_epoch` | UInt64 | Filter generation at capture time | | `epoll_op` | String | `epoll_ctl` operation (`ADD`/`MOD`/`DEL`); empty for other syscalls | @@ -89,6 +90,7 @@ bytes UInt64 address_space_bytes UInt64 requested_sleep_ns Int64 file String +old_file String is_error Bool filter_epoch UInt64 epoll_op String @@ -230,6 +232,6 @@ PARQUET_FILE=ior-recording-20260313-170234.parquet env GOTOOLCHAIN=auto mage par ``` It checks: -1. All 20 expected columns are present +1. All 21 expected columns are present 2. Row count > 0 3. `seq` is monotonically ordered and `time_ns` is non-zero diff --git a/integrationtests/rename_test.go b/integrationtests/rename_test.go index b64ea56..31012f2 100644 --- a/integrationtests/rename_test.go +++ b/integrationtests/rename_test.go @@ -1,6 +1,9 @@ package integrationtests -import "testing" +import ( + "strings" + "testing" +) func TestRenameBasic(t *testing.T) { runScenario(t, "rename-basic", []ExpectedEvent{ @@ -56,3 +59,46 @@ func TestRenameNoreplace(t *testing.T) { }, }) } + +// TestRenameRenameatOldnameInParquet locks in the oldname capture end-to-end +// for an AT-variant. renameat passes the source path at args[1] (after the +// olddfd dirfd), so this validates the args[1] oldname index: the new `old_file` +// parquet column must carry the source path while `file` carries the new path. +// A wrong-oldname-index regression would surface here as a missing/empty +// old_file, which no prior persisted-output test could detect. +func TestRenameRenameatOldnameInParquet(t *testing.T) { + h := newTestHarness(t) + path, pid, err := h.RunParquetWithIorArgs("rename-renameat", defaultDuration, + []string{"-trace-syscalls", "renameat"}) + if err != nil { + t.Fatalf("run rename-renameat parquet scenario: %v", err) + } + + rows := filterRecordsByPID(readParquetRecords(t, path), uint32(pid)) + if len(rows) == 0 { + t.Fatalf("expected parquet rows for workload PID %d", pid) + } + + var sawRenameat bool + for _, row := range rows { + if row.Syscall != "renameat" { + // old_file is rename/link-only; everything else must leave it empty. + if row.OldFile != "" { + t.Fatalf("%s row has unexpected old_file %q", row.Syscall, row.OldFile) + } + continue + } + sawRenameat = true + // file == newname; old_file == oldname (captured at args[1]). + if !strings.Contains(row.File, "renameat-new.txt") { + t.Fatalf("renameat row file = %q, want it to contain renameat-new.txt", row.File) + } + if !strings.Contains(row.OldFile, "renameat-old.txt") { + t.Fatalf("renameat row old_file = %q, want it to contain renameat-old.txt (args[1] oldname capture)", row.OldFile) + } + } + + if !sawRenameat { + t.Fatalf("expected at least one renameat row in parquet output") + } +} diff --git a/internal/event/pair.go b/internal/event/pair.go index afc9bed..3adf38b 100644 --- a/internal/event/pair.go +++ b/internal/event/pair.go @@ -37,6 +37,13 @@ type Pair struct { // Epoll.TargetFD is the descriptor being registered/modified/removed. Epoll EpollCtl HasEpoll bool + // Oldname holds the source/old path for rename-family (rename/renameat/ + // renameat2) and link-family (link/linkat/symlink/symlinkat) syscalls. The + // Pair-level File resolves to the "new" path (File.Name() == newname), so + // Oldname is the only place the captured source path (BPF name_event.oldname, + // at args[1] for the AT-variants after a dirfd) reaches the output schema. + // Empty for every other syscall. + Oldname string } // EpollCtl holds the decoded epoll_ctl arguments surfaced from the BPF diff --git a/internal/eventloop_exit.go b/internal/eventloop_exit.go index 105d9ac..9802a6f 100644 --- a/internal/eventloop_exit.go +++ b/internal/eventloop_exit.go @@ -81,7 +81,12 @@ func (e *eventLoop) handleExecExit(ep *event.Pair, execEv *types.ExecEvent) bool } func (e *eventLoop) handleNameExit(ep *event.Pair, nameEv *types.NameEvent) bool { + // File.Name() resolves to the "new" path (newname); surface the captured + // source path (oldname, at args[1] for the AT-variants) separately on the + // Pair so it reaches the output schema rather than living only in the + // TUI String() repr ("old:... ->new:..."). ep.File = file.NewOldnameNewname(nameEv.Oldname[:], nameEv.Newname[:]) + ep.Oldname = types.StringValue(nameEv.Oldname[:]) ep.Comm = e.comm(nameEv.GetTid()) return true } diff --git a/internal/parquet/schema.go b/internal/parquet/schema.go index b7ed381..8a92ea4 100644 --- a/internal/parquet/schema.go +++ b/internal/parquet/schema.go @@ -30,6 +30,12 @@ type Record struct { File string `parquet:"file"` IsError bool `parquet:"is_error"` FilterEpoch uint64 `parquet:"filter_epoch"` + // OldFile is the source/old path for rename-family (rename/renameat/ + // renameat2) and link-family (link/linkat/symlink/symlinkat) syscalls; the + // `file` column carries the "new" path. This is the only place the captured + // oldname (BPF name_event.oldname, at args[1] for the AT-variants) is + // persisted. Empty for every other syscall. + OldFile string `parquet:"old_file"` // EpollOp/EpollTargetFD/EpollEvents surface epoll_ctl control metadata: the // operation (ADD/MOD/DEL), the target descriptor registered (args[2]), and // the requested event mask (args[3]->events). EpollOp is empty and the @@ -81,6 +87,7 @@ func RecordFromStream(row streamrow.Row, filterEpoch uint64) Record { File: row.FileName, IsError: row.IsError, FilterEpoch: filterEpoch, + OldFile: row.OldName, EpollOp: row.EpollOp, EpollTargetFD: row.EpollTargetFD, EpollEvents: row.EpollEvents, diff --git a/internal/streamrow/row.go b/internal/streamrow/row.go index c846346..7ed0520 100644 --- a/internal/streamrow/row.go +++ b/internal/streamrow/row.go @@ -37,6 +37,10 @@ type Row struct { EpollOp string EpollTargetFD int32 EpollEvents uint32 + // OldName is the source/old path for rename-family (rename/renameat/ + // renameat2) and link-family (link/linkat/symlink/symlinkat) syscalls; + // FileName carries the "new" path. Empty for every other syscall. + OldName string } func (r Row) SyscallValue() string { @@ -124,6 +128,9 @@ func New(seq uint64, pair *event.Pair) Row { AddressSpaceBytes: pair.AddressSpaceBytes, RequestedSleepNs: pair.RequestedSleepNs, FD: UnknownFD, + // OldName carries the rename/link source path; FileName is the new path. + // Empty for non-rename/link syscalls (pair.Oldname is zero there). + OldName: pair.Oldname, } if fd, ok := pair.FileDescriptor(); ok { row.FD = fd |
