Skip to content

Commit 297a157

Browse files
authored
fix: panic caused by nil Writer provided to Logger (#1228)
* fix: panic caused by nil Writer provided to Logger In the log.(*Logger).Init method, several different strategies are considered based on the configuration provided by the user. In one common case, a rotating log file is opened and the log output is tee-ed into it. If there is any issue with opening that log file, the `log.(*Log).SetOutput` method is never invoked on the underlying logger. Since this standard library logger was created with `nil` passed as the `io.Writer`, this results in a nil pointer deref panic. The error causing the issue is also not surfaced to the user. This initializes the standard library logger with the `io.Discard` writer. This adheres to the original intention here, while also being a safer alternative to nil (effectively writes are to /dev/null). Also, the error is written out to stderr, in the hopes that an operator will be able to diagnose and fix the issue (in the absense of a correct logger configuration). Finally, the logger initialization has been moved to a new function, `log.NewLoggerE` that properly exposes the underlying error. The original function, `NewLogger`, has been modified to use this function instead and has been marked Deprecated. * Remove redundant types in function signatures The linter correctly pointed out that the types are redundant here. While ordinarily I tend to leave them if the ellision appears in the middle of the signature, I respect the linter's decision :) * Change NewLoggerE test to use TempDir Rather than trying to pull a non-existant path out of thin air and hoping that it doesn't exist, this instead uses os.TempDir to create a guaranteed-empty directory. From within the newly-created tempdir we try to use a non-existent path. This is better both on a single platform, but also has the benefit of working well on Windows too. * Fix test to use os.MkdirTemp instead of os.TempDir The intention of this test was to create a new temporary directory rather than use the system's temporary directory. This mistake was pointed out by @rbtr in a code review (thanks!). * Fix nil logger under error conditions When an error is present, we want to return both the error and the logger to guarantee that a usable logger is always present. This returns the logger in all circumstances and documents that fact in the godoc for this function.
1 parent 1ea2f5a commit 297a157

File tree

2 files changed

+64
-12
lines changed

2 files changed

+64
-12
lines changed

log/logger.go

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,44 @@ type Logger struct {
5858

5959
var pid = os.Getpid()
6060

61-
// NewLogger creates a new Logger.
62-
func NewLogger(name string, level int, target int, logDir string) *Logger {
63-
var logger Logger
61+
// NewLoggerE creates a new Logger and surfaces any errors encountered during
62+
// the process. The returned logger is guaranteed to be safe to use when a
63+
// non-nil error is returned, but may have undesired behavior. Callers should
64+
// treat the logger as nil under error conditions unless necessary for
65+
// backwards compatibility reasons.
66+
func NewLoggerE(name string, level, target int, logDir string) (*Logger, error) {
67+
logger := &Logger{
68+
l: log.New(io.Discard, logPrefix, log.LstdFlags),
69+
name: name,
70+
level: level,
71+
directory: logDir,
72+
maxFileSize: maxLogFileSize,
73+
maxFileCount: maxLogFileCount,
74+
mutex: &sync.Mutex{},
75+
}
6476

65-
logger.l = log.New(nil, logPrefix, log.LstdFlags)
66-
logger.name = name
67-
logger.level = level
68-
logger.directory = logDir
69-
logger.SetTarget(target)
70-
logger.maxFileSize = maxLogFileSize
71-
logger.maxFileCount = maxLogFileCount
72-
logger.mutex = &sync.Mutex{}
77+
err := logger.SetTarget(target)
78+
if err != nil {
79+
// we *do* want to return the logger here for backwards compatibility
80+
return logger, fmt.Errorf("setting log target: %w", err)
81+
}
82+
return logger, nil
83+
}
84+
85+
// NewLogger creates a new Logger.
86+
//
87+
// Deprecated: use NewLoggerE instead
88+
func NewLogger(name string, level, target int, logDir string) *Logger {
89+
logger, err := NewLoggerE(name, level, target, logDir)
90+
if err != nil {
91+
// ideally this would be returned to the caller, but this API is depended
92+
// on by unknown parties. Given at this point we have an unusable (but
93+
// safe) logger, we log to stderr with the standard library in hopes that
94+
// an operator will see the error and be able to take corrective action
95+
log.Println("error initializing logger: err:", err)
96+
}
7397

74-
return &logger
98+
return logger
7599
}
76100

77101
// SetName sets the log name.

log/logger_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package log
66
import (
77
"fmt"
88
"os"
9+
"path"
910
"strings"
1011
"testing"
1112
)
@@ -14,6 +15,33 @@ const (
1415
logName = "test"
1516
)
1617

18+
func TestNewLoggerError(t *testing.T) {
19+
// we expect an error from NewLoggerE in the event that we provide an
20+
// unwriteable directory
21+
22+
// this test needs a guaranteed empty directory, so we create a temporary one
23+
// and ensure that it gets destroyed afterward.
24+
targetDir, err := os.MkdirTemp("", "acn")
25+
if err != nil {
26+
t.Fatal("unable to create temporary directory: err:", err)
27+
}
28+
29+
t.Cleanup(func() {
30+
// This removal could produce an error, but since it's a temporary
31+
// directory anyway, this is a best-effort cleanup
32+
os.Remove(targetDir)
33+
})
34+
35+
// if we just use the targetDir, NewLoggerE will create the file and it will
36+
// work. We need a non-existent directory *within* the tempdir
37+
fullPath := path.Join(targetDir, "definitelyDoesNotExist")
38+
39+
_, err = NewLoggerE(logName, LevelInfo, TargetLogfile, fullPath)
40+
if err == nil {
41+
t.Error("expected an error but did not receive one")
42+
}
43+
}
44+
1745
// Tests that the log file rotates when size limit is reached.
1846
func TestLogFileRotatesWhenSizeLimitIsReached(t *testing.T) {
1947
logDirectory := "" // This sets the current location for logs

0 commit comments

Comments
 (0)