deps: remove lfshook in favor of local fileHook#2464
deps: remove lfshook in favor of local fileHook#2464
Conversation
Replace github.com/rifflock/lfshook with a minimal fileHook that implements logrus.Hook. The hook writes log entries to a file using logrus.TextFormatter. lfshook is now fully removed from the module. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verify that fileHook correctly implements logrus.Hook interface, writes formatted entries to file, and appends on multiple calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR removes the unmaintained lfshook dependency and replaces it with a small in-repo logrus.Hook implementation that writes log entries to a local file.
Changes:
- Removed
github.com/rifflock/lfshookfromgo.mod/go.sum. - Added a
fileHooktype implementinglogrus.Hookand swapped hook registration to use it. - Added unit/integration tests validating
fileHookbehavior.
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| logging/logutil.go | Replaces lfshook usage with a local fileHook implementation and registers it on the logger. |
| logging/filehook_test.go | Adds tests to validate fileHook levels, write behavior, append behavior, and end-to-end integration. |
| go.mod | Removes the lfshook requirement. |
| go.sum | Removes lfshook checksums after go mod tidy. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| path := filepath.Join(dir, fmt.Sprintf("%s.log", whereami)) | ||
| if _, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil { | ||
| log.Hooks.Add(lfshook.NewHook(lfshook.PathMap{ | ||
| logrus.DebugLevel: path, | ||
| logrus.InfoLevel: path, | ||
| logrus.WarnLevel: path, | ||
| logrus.ErrorLevel: path, | ||
| logrus.FatalLevel: path, | ||
| logrus.PanicLevel: path, | ||
| }, nil)) | ||
| log.Hooks.Add(&fileHook{ | ||
| path: path, | ||
| formatter: &logrus.TextFormatter{DisableColors: true}, | ||
| }) | ||
| } else { |
There was a problem hiding this comment.
The os.OpenFile call opens a file descriptor that is never closed (the returned *os.File is discarded). This can leak a FD per logger initialization. Use os.OpenFile to get f, close it (or use os.OpenFile inside a small scope and defer f.Close()), or replace this check with an operation that doesn't allocate an FD (e.g., attempt to open with proper close, or rely on the hook's Fire() to surface errors).
There was a problem hiding this comment.
Good catch. The os.OpenFile at line 119 (in NewCustomLogger) was only used to verify the file could be created, but the returned *os.File was discarded without closing. Fixed: renamed to checkFile and added checkFile.Close() immediately after the check succeeds.
| func (h *fileHook) Fire(entry *logrus.Entry) error { | ||
| f, err := os.OpenFile(h.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer f.Close() | ||
|
|
||
| b, err := h.formatter.Format(entry) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| _, err = f.Write(b) | ||
| return err |
There was a problem hiding this comment.
f.Write(b) can legally perform a short write without returning an error, which would truncate log entries. Consider looping until all bytes are written (or use an io.Copy-style approach) and also consider capturing/returning Close() errors (currently ignored via defer f.Close()).
| func (h *fileHook) Fire(entry *logrus.Entry) error { | |
| f, err := os.OpenFile(h.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) | |
| if err != nil { | |
| return err | |
| } | |
| defer f.Close() | |
| b, err := h.formatter.Format(entry) | |
| if err != nil { | |
| return err | |
| } | |
| _, err = f.Write(b) | |
| return err | |
| func (h *fileHook) Fire(entry *logrus.Entry) (retErr error) { | |
| f, err := os.OpenFile(h.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) | |
| if err != nil { | |
| return err | |
| } | |
| defer func() { | |
| if cerr := f.Close(); cerr != nil && retErr == nil { | |
| retErr = cerr | |
| } | |
| }() | |
| b, err := h.formatter.Format(entry) | |
| if err != nil { | |
| return err | |
| } | |
| total := 0 | |
| for total < len(b) { | |
| n, werr := f.Write(b[total:]) | |
| if werr != nil { | |
| return werr | |
| } | |
| if n == 0 { | |
| return io.ErrShortWrite | |
| } | |
| total += n | |
| } | |
| return nil |
There was a problem hiding this comment.
Not fixing. Per the io.Writer contract, a short write must return a non-nil error (Write must return a non-nil error if it returns n < len(p)). The *os.File.Write implementation follows this contract. Adding a write loop would be dead code since any short write already surfaces as an error. Similarly, defer f.Close() already handles close — capturing the close error in a named return would add complexity with no practical benefit for a log hook.
| } | ||
|
|
||
| func (h *fileHook) Levels() []logrus.Level { | ||
| return logrus.AllLevels |
There was a problem hiding this comment.
Returning logrus.AllLevels directly exposes a shared slice; a caller could mutate it and affect global behavior. Prefer returning a copy (e.g., copy into a new slice) to prevent accidental mutation.
| return logrus.AllLevels | |
| levels := make([]logrus.Level, len(logrus.AllLevels)) | |
| copy(levels, logrus.AllLevels) | |
| return levels |
There was a problem hiding this comment.
Not fixing. fileHook is an unexported type, so Levels() is only called by the logrus hook dispatch internally. No external caller can mutate the returned slice. Defensive copying would add an allocation on every log entry for a scenario that cannot occur in practice.
logging/filehook_test.go
Outdated
|
|
||
| func TestFileHookLevels(t *testing.T) { | ||
| h := &fileHook{ | ||
| path: "/dev/null", |
There was a problem hiding this comment.
Using \"/dev/null\" will fail on non-Unix platforms. Use os.DevNull for a portable sink path.
| path: "/dev/null", | |
| path: os.DevNull, |
There was a problem hiding this comment.
Good point. Changed to os.DevNull for cross-platform portability.
logging/logutil.go
Outdated
| } | ||
|
|
||
| func (h *fileHook) Fire(entry *logrus.Entry) error { | ||
| f, err := os.OpenFile(h.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) |
There was a problem hiding this comment.
Creating log files with mode 0644 makes them readable by other users on the host, which can expose sensitive data if logs contain secrets/tokens. Consider using 0600 by default (or making the mode configurable) to reduce unintended disclosure.
| f, err := os.OpenFile(h.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) | |
| f, err := os.OpenFile(h.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) |
There was a problem hiding this comment.
Agreed. Changed all log file OpenFile calls from 0644 to 0600 — both in the fileHook.Fire method and in NewCustomLogger (for vuls.log and the per-host log file).
- Close FD leak in NewCustomLogger (checkFile.Close()) - Use os.DevNull instead of /dev/null in test for portability - Tighten file permissions from 0644 to 0600 for all log files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 4 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Why (motivation for removing this dependency)
lfshookis a logrus hook that writes log entries to files based on log levellogrus.Hookimplementation. The library is not actively maintained (last release 2019)go mod tidyeliminates it entirelyWhat (replacement details)
lfshook.NewHook(PathMap, nil)with a 20-linefileHookstruct implementinglogrus.Hooklogging/logutil.go: removedlfshookimport, addedfileHooktype withLevels()andFire()methods, replaced the hook registration callSafety (why this is safe)
fileHook.Levels()returnslogrus.AllLevels(same as the original PathMap mapping all levels)fileHook.Fire()opens the file in append mode, formats the entry withlogrus.TextFormatter, and writes — same behavior as lfshook's internal logicDisableColors: truein the formatter to match lfshook's default behavior (no ANSI codes in file output)Test plan
TestFileHookLevels- verifiesLevels()returns all logrus levelsTestFileHookFire- verifiesFire()writes a formatted entry to the fileTestFileHookFireAppends- verifies multipleFire()calls append (not overwrite)TestFileHookIntegration- end-to-end: adds hook to logrus logger, logs a message, verifies file outputgo build ./cmd/...passgo test ./logging/...passReview hint (how to review efficiently)
fileHookstruct +Levels()+Fire()(20 lines) — this is the entire replacementlfshook.NewHook(PathMap{...}, nil)becomes&fileHook{path: path, formatter: &logrus.TextFormatter{DisableColors: true}}go mod why -m github.com/rifflock/lfshook🤖 Generated with Claude Code