Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ require (
github.com/package-url/packageurl-go v0.1.5
github.com/parnurzeal/gorequest v0.3.0
github.com/pkg/errors v0.9.1
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
github.com/samber/lo v1.52.0
github.com/sirupsen/logrus v1.9.4
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -770,8 +770,6 @@ github.com/redis/rueidis v1.0.70 h1:O01v0Mt27/qXV9mKU/zahgxHdC8piHzIepqW4Nyzn/I=
github.com/redis/rueidis v1.0.70/go.mod h1:lfdcZzJ1oKGKL37vh9fO3ymwt+0TdjkkUCJxbgpmcgQ=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
Expand Down
131 changes: 131 additions & 0 deletions logging/filehook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package logging

import (
"os"
"strings"
"testing"

"github.com/sirupsen/logrus"
)

func TestFileHookLevels(t *testing.T) {
h := &fileHook{
path: os.DevNull,
formatter: &logrus.TextFormatter{},
}
got := h.Levels()
if len(got) != len(logrus.AllLevels) {
t.Errorf("Levels() returned %d levels, want %d", len(got), len(logrus.AllLevels))
}
for i, l := range logrus.AllLevels {
if got[i] != l {
t.Errorf("Levels()[%d] = %v, want %v", i, got[i], l)
}
}
}

func TestFileHookFire(t *testing.T) {
tmpFile, err := os.CreateTemp(t.TempDir(), "filehook-*.log")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
if err := tmpFile.Close(); err != nil {
t.Fatalf("failed to close temp file: %v", err)
}

h := &fileHook{
path: tmpFile.Name(),
formatter: &logrus.TextFormatter{DisableColors: true, DisableTimestamp: true},
}

entry := &logrus.Entry{
Logger: logrus.New(),
Level: logrus.InfoLevel,
Message: "hello from fire",
Data: logrus.Fields{},
}

if err := h.Fire(entry); err != nil {
t.Fatalf("Fire() returned error: %v", err)
}

content, err := os.ReadFile(tmpFile.Name())
if err != nil {
t.Fatalf("failed to read log file: %v", err)
}
if !strings.Contains(string(content), "hello from fire") {
t.Errorf("log file does not contain expected message, got: %q", string(content))
}
}

func TestFileHookFireAppends(t *testing.T) {
tmpFile, err := os.CreateTemp(t.TempDir(), "filehook-*.log")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
if err := tmpFile.Close(); err != nil {
t.Fatalf("failed to close temp file: %v", err)
}

h := &fileHook{
path: tmpFile.Name(),
formatter: &logrus.TextFormatter{DisableColors: true, DisableTimestamp: true},
}

for i, msg := range []string{"first message", "second message", "third message"} {
entry := &logrus.Entry{
Logger: logrus.New(),
Level: logrus.InfoLevel,
Message: msg,
Data: logrus.Fields{},
}
if err := h.Fire(entry); err != nil {
t.Fatalf("Fire() call %d returned error: %v", i, err)
}
}

content, err := os.ReadFile(tmpFile.Name())
if err != nil {
t.Fatalf("failed to read log file: %v", err)
}
s := string(content)
for _, msg := range []string{"first message", "second message", "third message"} {
if !strings.Contains(s, msg) {
t.Errorf("log file missing %q, got: %q", msg, s)
}
}
}

func TestFileHookIntegration(t *testing.T) {
tmpFile, err := os.CreateTemp(t.TempDir(), "filehook-*.log")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
if err := tmpFile.Close(); err != nil {
t.Fatalf("failed to close temp file: %v", err)
}

logger := logrus.New()
logger.Out = os.Stderr
logger.Level = logrus.DebugLevel

logger.Hooks.Add(&fileHook{
path: tmpFile.Name(),
formatter: &logrus.TextFormatter{DisableColors: true, DisableTimestamp: true},
})

logger.Info("integration info message")
logger.Warn("integration warn message")

content, err := os.ReadFile(tmpFile.Name())
if err != nil {
t.Fatalf("failed to read log file: %v", err)
}
s := string(content)
if !strings.Contains(s, "integration info message") {
t.Errorf("log file missing info message, got: %q", s)
}
if !strings.Contains(s, "integration warn message") {
t.Errorf("log file missing warn message, got: %q", s)
}
}
42 changes: 31 additions & 11 deletions logging/logutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,35 @@
"runtime"

"github.com/k0kubun/pp"
"github.com/rifflock/lfshook"
"github.com/sirupsen/logrus"

formatter "github.com/kotakanbe/logrus-prefixed-formatter"
)

type fileHook struct {
path string
formatter logrus.Formatter
}

func (h *fileHook) Levels() []logrus.Level {
return logrus.AllLevels
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
return logrus.AllLevels
levels := make([]logrus.Level, len(logrus.AllLevels))
copy(levels, logrus.AllLevels)
return levels

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

}

func (h *fileHook) Fire(entry *logrus.Entry) error {
f, err := os.OpenFile(h.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
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
Comment on lines +26 to +38
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()).

Suggested change
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

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

}

// LogOpts has options for logging
type LogOpts struct {
Debug bool `json:"debug,omitempty"`
Expand Down Expand Up @@ -84,7 +107,7 @@
}

logFile := dir + "/vuls.log"
if file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil {
if file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600); err == nil {
log.Out = io.MultiWriter(os.Stderr, file)
} else {
log.Out = os.Stderr
Expand All @@ -93,15 +116,12 @@

if _, err := os.Stat(dir); err == nil {
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))
if checkFile, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600); err == nil {
checkFile.Close()

Check failure on line 120 in logging/logutil.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `checkFile.Close` is not checked (errcheck)
log.Hooks.Add(&fileHook{
path: path,
formatter: &logrus.TextFormatter{DisableColors: true},
})
} else {
Comment on lines 118 to 125
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

log.Errorf("Failed to create log file. path: %s, err: %+v", path, err)
}
Expand Down
Loading