diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-09 22:24:30 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-09 22:24:30 +0300 |
| commit | f601dc90fcef3f270c55a9612c5f0326dbd0f391 (patch) | |
| tree | f77fb46bebc8263fdac31ae1f517dd82ed8ddf15 /internal | |
| parent | 7031211501884555139351bb676fc0592c9df14c (diff) | |
feat(parquet): export rename/link oldname + assert oldname capture end-to-end
rename-family (rename/renameat/renameat2) and link-family (link/linkat/
symlink/symlinkat) capture BOTH paths in BPF (name_event.oldname at
args[1] for the AT-variants, after a dirfd, plus newname), but only
newname reached any persisted output. event.Pair.FileName() resolves to
oldnameNewnameFile.Name() == Newname, so the parquet `file` column, CSV,
and flamegraph Path all carried newname; the captured oldname survived
only in the TUI stream String() repr ("old:... ->new:..."). The oldname
capture (and its args[1] index for the AT-variants) was therefore never
validated end-to-end, and a wrong-oldname-index regression would surface
in no persisted output or test.
Surface oldname as one additive, backward-compatible column, mirroring
the dedicated optional-column convention (address_space_bytes,
requested_sleep_ns, epoll_*):
- old_file (String): source/old path for rename/link syscalls; empty for
every other syscall. The existing `file` column keeps its semantics
(newname for rename/link).
Data flows name_event.oldname -> event.Pair.Oldname (populated in
handleNameExit alongside the existing File) -> streamrow.Row.OldName ->
parquet.Record.OldFile. Docs (docs/parquet-querying.md) and the Magefile
parquetValidate column list updated in lockstep.
The new TestRenameRenameatOldnameInParquet integration test exercises the
renameat AT-variant (oldname at args[1] after the olddfd dirfd) and
asserts the parquet old_file column carries renameat-old.txt while file
carries renameat-new.txt, and that old_file stays empty for non-rename/
link rows -- locking in the args[1] oldname capture end-to-end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'internal')
| -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 |
4 files changed, 26 insertions, 0 deletions
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 |
