Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

Commit

Permalink
lib/log: internalize logger flushing (#34323)
Browse files Browse the repository at this point in the history
This PR removes Sync() from the logger interface and adds a callback returned by log.Init which should be called before application exit. The removal of Sync() from the Logger interface helps indicate that not every logger from log.Scoped needs a call to Sync(). For logtest, we add a t.Cleanup func to sync the global logger.

Additionally, I've noticed that log.Fatal is used quite liberally. Even if we switch to error propagation, there's still the issue that os.Exit will bypass any defer calls and hence potentially miss Sync(). To accommodate this pattern, I've decided to add back Fatal to Logger, which internally calls Sync() before os.Exit(1). Also see uber-go/zap#207 for more rationale for supporting Fatal.

Background: All loggers are children of the root global logger that is instantiated once. As such, it is sufficient to just call Sync() on the root logger - all child loggers just write to underlying loggers until they reach the root, which as far as I can tell is an os.File that has Sync(), which gets called by Zap, so it's not a no-op even if Zap itself does not do any buffering (at the moment, the global logger is not configured to do so)

Aside: in practice, it's honestly probably not a huge deal to never call Sync(), but we should anyway just in case, so hopefully this balances the probable need to call Sync() with ergonomics.
  • Loading branch information
bobheadxi authored Apr 22, 2022
1 parent 7db5847 commit 6153851
Show file tree
Hide file tree
Showing 5 changed files with 18 additions and 12 deletions.
6 changes: 4 additions & 2 deletions doc/dev/how-to/add_and_use_logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,16 @@ import (

func main() {
// If unintialized, calls to `log.Scoped` will return a no-op logger in production, or
// panic in development.
// panic in development. It returns a callback to flush the logger buffer, if any, that
// you should make sure to call before application exit (namely via `defer`)
//
// Repeated calls to `log.Init` will panic. Make sure to call this exactly once in `main`!
log.Init(log.Resource{
syncLogs := log.Init(log.Resource{
Name: env.MyName,
/* ... optional fields */
Version: version.Version(),
})
defer syncLogs()

service.Start(/* ... */)
}
Expand Down
7 changes: 4 additions & 3 deletions lib/log/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,19 @@ type Resource = otfields.Resource

// Init initializes the log package's global logger as a logger of the given resource.
// It must be called on service startup, i.e. 'main()', NOT on an 'init()' function.
//
// Subsequent calls will panic, so do not call this within a non-service context.
//
// Init returns a callback, sync, that should be called before application exit.
//
// For testing, you can use 'logtest.Init' to initialize the logging library.
//
// If Init is not called, Get will panic.
func Init(r Resource) {
func Init(r Resource) (sync func() error) {
if globallogger.IsInitialized() {
panic("log.Init initialized multiple times")
}

level := zap.NewAtomicLevelAt(Level(os.Getenv(envSrcLogLevel)).Parse())
format := encoders.ParseOutputFormat(os.Getenv(envSrcLogFormat))
globallogger.Init(r, level, format, development)
return globallogger.Init(r, level, format, development)
}
6 changes: 4 additions & 2 deletions lib/log/internal/globallogger/globallogger.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ func Get(safe bool) *zap.Logger {
return globalLogger
}

// Init initializes the global logger once. Subsequent calls are no-op.
func Init(r otfields.Resource, level zap.AtomicLevel, format encoders.OutputFormat, development bool) {
// Init initializes the global logger once. Subsequent calls are no-op. Returns the
// callback to sync the root core.
func Init(r otfields.Resource, level zap.AtomicLevel, format encoders.OutputFormat, development bool) func() error {
globalLoggerInit.Do(func() {
globalLogger = initLogger(r, level, format, development)
})
return globalLogger.Sync
}

// IsInitialized indicates if the global logger is initialized.
Expand Down
7 changes: 3 additions & 4 deletions lib/log/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,9 @@ type Logger interface {
// Error logs are high-priority. If an application is running smoothly, it shouldn't
// generate any error-level logs.
Error(string, ...Field)

// Sync flushes any buffered log entries. Applications should take care to call Sync
// before exiting.
Sync() error
// Fatal logs a fatal error message, including any fields accumulated on the Logger.
// The logger then calls os.Exit(1), flushing the logger before doing so. Use sparingly.
Fatal(string, ...Field)
}

// Scoped returns the global logger and sets it up with the given scope and OpenTelemetry
Expand Down
4 changes: 3 additions & 1 deletion lib/log/logtest/logtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ func Scoped(t testing.TB) log.Logger {
// already been done. We allow this in testing for convenience.
Init(nil)

// On cleanup, flush the global logger.
t.Cleanup(func() { globallogger.Get(true).Sync() })

return log.Scoped(t.Name(), "")
}

Expand All @@ -88,7 +91,6 @@ func Captured(t testing.TB) (logger log.Logger, exportLogs func() []CapturedLog)
}))

return logger, func() []CapturedLog {
logger.Sync()
entries := entries.TakeAll()
logs := make([]CapturedLog, len(entries))
for i, e := range entries {
Expand Down

0 comments on commit 6153851

Please sign in to comment.