Begin implementation of p4lf following the design decided in Session 1. No code existed at the start of this session.
Use Go + custom file reader (not go-libtail, not Bash + p4 logtail). See session_log_2026-06-25.md for the full rationale.
Development is on macOS (Apple Silicon, Go 1.24.5). Target runtime is Linux
(amd64). Go cross-compiles trivially; all code uses only portable stdlib plus
fsnotify. Integration/system testing (systemd, real p4d) will be done on
Linux in a later session.
github.com/rcowham/p4lfgithub.com/fsnotify/fsnotify v1.10.1
(for inotify-backed file watching; abstracts OS differences)p4lf/
cmd/p4lf/main.go — entry point, signal handling, main loop
internal/config/config.go — config parser (KEY=VALUE format)
internal/config/config_test.go — unit tests for config
internal/tailer/tailer.go — file reader, rotation detection, state file
internal/chunker/chunker.go — chunk assembly, gzip, disk guards
internal/chunker/chunker_test.go — unit tests for chunker
internal/logger/logger.go — p4lf's own rotating log writer
go.mod / go.sum
p4lf.cfg.example — documented example config
p4lf.service — systemd unit file
| Setting | Notes |
|---|---|
P4LogFile |
Path to P4LOG; also read from $P4LOG env var |
LogTailDelay |
Chunk flush interval; format: 60s / 3m / 1h |
LogChunksDir |
Output directory for log.*.gz files |
MaxLogChunks |
Max un-consumed chunks before pausing |
MinLogSpace |
Min free space (percent or absolute) before pausing |
MaxLogSize |
p4lf's own log rotation threshold |
MaxRotationRecoverySize |
Max new P4LOG size before giving up on reading from beginning post-rotation |
StateFile |
Checkpoint: inode + byte offset, JSON, atomic write |
ReadFromStart |
First-run behaviour: BOF (true) or EOF (false) |
Debug |
Verbosity: 0/1/2 |
Flush() call, stat the P4LOG path (not the open fd)saved_offset > current_file_size (handles copytruncate).tmp, then os.Rename){"inode": 12345, "offset": 6789}log.<startOffset>.<endOffset>.gz
Written via:
P4LOG_chunk.<o1>.<o2>.gz.tmpos.Rename to log.<o1>.<o2>.gz (atomic)SIGTERM / SIGINT: final flush, save state, clean exitSIGHUP: reload config + rotate p4lf's own logWaitForFile() blocks at startup until P4LOG existsok github.com/rcowham/p4lf/internal/config 0.430s
ok github.com/rcowham/p4lf/internal/chunker 0.265s
Config tests cover: defaults, all settings, duration formats (s/m/h/plain), size formats (K/M/G/T/None/0), percent space spec, absolute space spec, missing P4LogFile, unknown key, comments, missing config file.
Chunker tests cover: gzip file creation, content correctness, MaxLogChunks guard.
Cross-compiled successfully: GOOS=linux GOARCH=amd64 → 3.8MB static binary.
All new files opened for add in the default changelist. Not yet submitted. Pending explicit permission to submit/shelve.
WaitUntilGuardsPassed in main loop should have a
maximum wait timeout before giving up and logging an error.# P4LF Session Log — 2026-06-25 (Session 2: Implementation)
## Session Goal
Begin implementation of p4lf following the design decided in Session 1.
No code existed at the start of this session.
## Key Decision Carried Forward
Use **Go + custom file reader** (not go-libtail, not Bash + p4 logtail).
See session_log_2026-06-25.md for the full rationale.
## Development Environment Note
Development is on macOS (Apple Silicon, Go 1.24.5). Target runtime is Linux
(amd64). Go cross-compiles trivially; all code uses only portable stdlib plus
`fsnotify`. Integration/system testing (systemd, real p4d) will be done on
Linux in a later session.
## What Was Built
### Go module
- Module name: `github.com/rcowham/p4lf`
- Single external dependency: `github.com/fsnotify/fsnotify v1.10.1`
(for inotify-backed file watching; abstracts OS differences)
### Directory structure
```
p4lf/
cmd/p4lf/main.go — entry point, signal handling, main loop
internal/config/config.go — config parser (KEY=VALUE format)
internal/config/config_test.go — unit tests for config
internal/tailer/tailer.go — file reader, rotation detection, state file
internal/chunker/chunker.go — chunk assembly, gzip, disk guards
internal/chunker/chunker_test.go — unit tests for chunker
internal/logger/logger.go — p4lf's own rotating log writer
go.mod / go.sum
p4lf.cfg.example — documented example config
p4lf.service — systemd unit file
```
### Config settings implemented
| Setting | Notes |
|---|---|
| `P4LogFile` | Path to P4LOG; also read from `$P4LOG` env var |
| `LogTailDelay` | Chunk flush interval; format: 60s / 3m / 1h |
| `LogChunksDir` | Output directory for log.*.gz files |
| `MaxLogChunks` | Max un-consumed chunks before pausing |
| `MinLogSpace` | Min free space (percent or absolute) before pausing |
| `MaxLogSize` | p4lf's own log rotation threshold |
| `MaxRotationRecoverySize` | Max new P4LOG size before giving up on reading from beginning post-rotation |
| `StateFile` | Checkpoint: inode + byte offset, JSON, atomic write |
| `ReadFromStart` | First-run behaviour: BOF (true) or EOF (false) |
| `Debug` | Verbosity: 0/1/2 |
### Rotation detection approach
- On every `Flush()` call, stat the P4LOG **path** (not the open fd)
- Compare returned inode to saved state inode
- If changed → rotation detected; capture trailing bytes from old fd, close it, open new file
- Also detect truncation: if `saved_offset > current_file_size` (handles `copytruncate`)
- State file written atomically (write to `.tmp`, then `os.Rename`)
- State file format: `{"inode": 12345, "offset": 6789}`
### Chunk file naming
```
log.<startOffset>.<endOffset>.gz
```
Written via:
1. Open temp file `P4LOG_chunk.<o1>.<o2>.gz.tmp`
2. Write gzip content
3. `os.Rename` to `log.<o1>.<o2>.gz` (atomic)
### Signal handling (main.go)
- `SIGTERM` / `SIGINT`: final flush, save state, clean exit
- `SIGHUP`: reload config + rotate p4lf's own log
### Service controls
- Detects config file modtime change each tick (in addition to SIGHUP)
- `WaitForFile()` blocks at startup until P4LOG exists
## Test Results
```
ok github.com/rcowham/p4lf/internal/config 0.430s
ok github.com/rcowham/p4lf/internal/chunker 0.265s
```
Config tests cover: defaults, all settings, duration formats (s/m/h/plain),
size formats (K/M/G/T/None/0), percent space spec, absolute space spec,
missing P4LogFile, unknown key, comments, missing config file.
Chunker tests cover: gzip file creation, content correctness, MaxLogChunks guard.
## Linux Build
Cross-compiled successfully: `GOOS=linux GOARCH=amd64` → 3.8MB static binary.
## Perforce Status
All new files opened for add in the default changelist. Not yet submitted.
Pending explicit permission to submit/shelve.
## What's Next (for the next session)
1. **Linux integration testing**: deploy to an SDP environment, run against
real p4d, verify rotation handling with actual log rotations.
2. **Tailer unit tests**: test rotation detection, state file checkpoint/resume,
truncation handling (requires temp file manipulation).
3. **Logger unit tests**: test rotation trigger at MaxLogSize, gzip of rotated log.
4. **Install/deploy script**: shell script or Makefile to install binary +
systemd unit to SDP paths.
5. **README update**: update README.md to reflect Go implementation and revised
config settings.
6. **Consider**: whether `WaitUntilGuardsPassed` in main loop should have a
maximum wait timeout before giving up and logging an error.
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #1 | 32818 | C. Thomas Tyler |
Initial implementation of p4lf in Go. Adds: - Go module (github.com/rcowham/p4lf) with fsnotify dependency - internal/config: KEY=VALUE config parser with all settings - internal/tailer: file reader with inode-based rotation detection and state file checkpoint/resume (inode + byte offset, JSON, atomic write) - internal/chunker: gzip chunk writer with MaxLogChunks/MinLogSpace guards - internal/logger: rotating log writer with gzip of rotated files - cmd/p4lf/main.go: service main loop, SIGHUP/SIGTERM/SIGINT handling, config hot-reload on modtime change - Makefile: build/test/install/release targets for Linux amd64/arm64, macOS arm64/amd64; version injected via ldflags - p4lf.cfg.example: fully documented example config - p4lf.service: systemd unit file (User=perforce, Restart=on-failure) - ai/session_log_2026-06-25.md: design session log - ai/session_log_2026-06-25-2.md: implementation session log - .p4ignore: added bin/ and dist/ |