summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Magefile.go2
-rw-r--r--docs/parquet-querying.md6
-rw-r--r--integrationtests/rename_test.go48
-rw-r--r--internal/event/pair.go7
-rw-r--r--internal/eventloop_exit.go5
-rw-r--r--internal/parquet/schema.go7
-rw-r--r--internal/streamrow/row.go7
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