Write all logging (INFO, WARN, ERROR) to stderr

The old setup tried to log >= warning to stderr, the rest to stdout.

However, that logic was flawed, so warnings ended up in stdout, which makes `hugo list all` etc. hard to reason about from scripts.

This commit fixes this by making all logging (info, warn, error) log to stderr and let stdout be reserved for program output.

Fixes #13074
This commit is contained in:
Bjørn Erik Pedersen 2024-12-13 09:23:09 +01:00 committed by GitHub
parent ec1933f79d
commit 9dfa112617
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 85 additions and 59 deletions

View file

@ -103,7 +103,8 @@ type configKey struct {
type rootCommand struct { type rootCommand struct {
Printf func(format string, v ...interface{}) Printf func(format string, v ...interface{})
Println func(a ...interface{}) Println func(a ...interface{})
Out io.Writer StdOut io.Writer
StdErr io.Writer
logger loggers.Logger logger loggers.Logger
@ -356,7 +357,7 @@ func (r *rootCommand) getOrCreateHugo(cfg config.Provider, ignoreModuleDoesNotEx
} }
func (r *rootCommand) newDepsConfig(conf *commonConfig) deps.DepsCfg { func (r *rootCommand) newDepsConfig(conf *commonConfig) deps.DepsCfg {
return deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, LogOut: r.logger.Out(), LogLevel: r.logger.Level(), ChangesFromBuild: r.changesFromBuild} return deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, StdOut: r.logger.StdOut(), StdErr: r.logger.StdErr(), LogLevel: r.logger.Level(), ChangesFromBuild: r.changesFromBuild}
} }
func (r *rootCommand) Name() string { func (r *rootCommand) Name() string {
@ -421,21 +422,23 @@ func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args
} }
func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error { func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
r.Out = os.Stdout r.StdOut = os.Stdout
r.StdErr = os.Stderr
if r.quiet { if r.quiet {
r.Out = io.Discard r.StdOut = io.Discard
r.StdErr = io.Discard
} }
// Used by mkcert (server). // Used by mkcert (server).
log.SetOutput(r.Out) log.SetOutput(r.StdOut)
r.Printf = func(format string, v ...interface{}) { r.Printf = func(format string, v ...interface{}) {
if !r.quiet { if !r.quiet {
fmt.Fprintf(r.Out, format, v...) fmt.Fprintf(r.StdOut, format, v...)
} }
} }
r.Println = func(a ...interface{}) { r.Println = func(a ...interface{}) {
if !r.quiet { if !r.quiet {
fmt.Fprintln(r.Out, a...) fmt.Fprintln(r.StdOut, a...)
} }
} }
_, running := runner.Command.(*serverCommand) _, running := runner.Command.(*serverCommand)
@ -485,8 +488,8 @@ func (r *rootCommand) createLogger(running bool) (loggers.Logger, error) {
optsLogger := loggers.Options{ optsLogger := loggers.Options{
DistinctLevel: logg.LevelWarn, DistinctLevel: logg.LevelWarn,
Level: level, Level: level,
Stdout: r.Out, StdOut: r.StdOut,
Stderr: r.Out, StdErr: r.StdErr,
StoreErrors: running, StoreErrors: running,
} }

View file

@ -57,7 +57,7 @@ func newListCommand() *listCommand {
return err return err
} }
writer := csv.NewWriter(r.Out) writer := csv.NewWriter(r.StdOut)
defer writer.Flush() defer writer.Flush()
writer.Write([]string{ writer.Write([]string{

View file

@ -40,8 +40,8 @@ func newNoAnsiEscapeHandler(outWriter, errWriter io.Writer, noLevelPrefix bool,
type noAnsiEscapeHandler struct { type noAnsiEscapeHandler struct {
mu sync.Mutex mu sync.Mutex
outWriter io.Writer // Defaults to os.Stdout. outWriter io.Writer
errWriter io.Writer // Defaults to os.Stderr. errWriter io.Writer
predicate func(*logg.Entry) bool predicate func(*logg.Entry) bool
noLevelPrefix bool noLevelPrefix bool
} }

View file

@ -38,8 +38,8 @@ var (
// Options defines options for the logger. // Options defines options for the logger.
type Options struct { type Options struct {
Level logg.Level Level logg.Level
Stdout io.Writer StdOut io.Writer
Stderr io.Writer StdErr io.Writer
DistinctLevel logg.Level DistinctLevel logg.Level
StoreErrors bool StoreErrors bool
HandlerPost func(e *logg.Entry) error HandlerPost func(e *logg.Entry) error
@ -48,21 +48,22 @@ type Options struct {
// New creates a new logger with the given options. // New creates a new logger with the given options.
func New(opts Options) Logger { func New(opts Options) Logger {
if opts.Stdout == nil { if opts.StdOut == nil {
opts.Stdout = os.Stdout opts.StdOut = os.Stdout
} }
if opts.Stderr == nil { if opts.StdErr == nil {
opts.Stderr = os.Stdout opts.StdErr = os.Stderr
} }
if opts.Level == 0 { if opts.Level == 0 {
opts.Level = logg.LevelWarn opts.Level = logg.LevelWarn
} }
var logHandler logg.Handler var logHandler logg.Handler
if terminal.PrintANSIColors(os.Stdout) { if terminal.PrintANSIColors(os.Stderr) {
logHandler = newDefaultHandler(opts.Stdout, opts.Stderr) logHandler = newDefaultHandler(opts.StdErr, opts.StdErr)
} else { } else {
logHandler = newNoAnsiEscapeHandler(opts.Stdout, opts.Stderr, false, nil) logHandler = newNoAnsiEscapeHandler(opts.StdErr, opts.StdErr, false, nil)
} }
errorsw := &strings.Builder{} errorsw := &strings.Builder{}
@ -137,7 +138,8 @@ func New(opts Options) Logger {
logCounters: logCounters, logCounters: logCounters,
errors: errorsw, errors: errorsw,
reset: reset, reset: reset,
out: opts.Stdout, stdOut: opts.StdOut,
stdErr: opts.StdErr,
level: opts.Level, level: opts.Level,
logger: logger, logger: logger,
tracel: l.WithLevel(logg.LevelTrace), tracel: l.WithLevel(logg.LevelTrace),
@ -153,8 +155,6 @@ func NewDefault() Logger {
opts := Options{ opts := Options{
DistinctLevel: logg.LevelWarn, DistinctLevel: logg.LevelWarn,
Level: logg.LevelWarn, Level: logg.LevelWarn,
Stdout: os.Stdout,
Stderr: os.Stdout,
} }
return New(opts) return New(opts)
} }
@ -163,8 +163,6 @@ func NewTrace() Logger {
opts := Options{ opts := Options{
DistinctLevel: logg.LevelWarn, DistinctLevel: logg.LevelWarn,
Level: logg.LevelTrace, Level: logg.LevelTrace,
Stdout: os.Stdout,
Stderr: os.Stdout,
} }
return New(opts) return New(opts)
} }
@ -189,7 +187,8 @@ type Logger interface {
Level() logg.Level Level() logg.Level
LoggCount(logg.Level) int LoggCount(logg.Level) int
Logger() logg.Logger Logger() logg.Logger
Out() io.Writer StdOut() io.Writer
StdErr() io.Writer
Printf(format string, v ...any) Printf(format string, v ...any)
Println(v ...any) Println(v ...any)
PrintTimerIfDelayed(start time.Time, name string) PrintTimerIfDelayed(start time.Time, name string)
@ -207,7 +206,8 @@ type logAdapter struct {
logCounters *logLevelCounter logCounters *logLevelCounter
errors *strings.Builder errors *strings.Builder
reset func() reset func()
out io.Writer stdOut io.Writer
stdErr io.Writer
level logg.Level level logg.Level
logger logg.Logger logger logg.Logger
tracel logg.LevelLogger tracel logg.LevelLogger
@ -259,8 +259,12 @@ func (l *logAdapter) Logger() logg.Logger {
return l.logger return l.logger
} }
func (l *logAdapter) Out() io.Writer { func (l *logAdapter) StdOut() io.Writer {
return l.out return l.stdOut
}
func (l *logAdapter) StdErr() io.Writer {
return l.stdErr
} }
// PrintTimerIfDelayed prints a time statement to the FEEDBACK logger // PrintTimerIfDelayed prints a time statement to the FEEDBACK logger
@ -279,11 +283,11 @@ func (l *logAdapter) Printf(format string, v ...any) {
if !strings.HasSuffix(format, "\n") { if !strings.HasSuffix(format, "\n") {
format += "\n" format += "\n"
} }
fmt.Fprintf(l.out, format, v...) fmt.Fprintf(l.stdOut, format, v...)
} }
func (l *logAdapter) Println(v ...any) { func (l *logAdapter) Println(v ...any) {
fmt.Fprintln(l.out, v...) fmt.Fprintln(l.stdOut, v...)
} }
func (l *logAdapter) Reset() { func (l *logAdapter) Reset() {

View file

@ -31,8 +31,8 @@ func TestLogDistinct(t *testing.T) {
opts := loggers.Options{ opts := loggers.Options{
DistinctLevel: logg.LevelWarn, DistinctLevel: logg.LevelWarn,
StoreErrors: true, StoreErrors: true,
Stdout: io.Discard, StdOut: io.Discard,
Stderr: io.Discard, StdErr: io.Discard,
} }
l := loggers.New(opts) l := loggers.New(opts)
@ -54,8 +54,8 @@ func TestHookLast(t *testing.T) {
HandlerPost: func(e *logg.Entry) error { HandlerPost: func(e *logg.Entry) error {
panic(e.Message) panic(e.Message)
}, },
Stdout: io.Discard, StdOut: io.Discard,
Stderr: io.Discard, StdErr: io.Discard,
} }
l := loggers.New(opts) l := loggers.New(opts)
@ -70,8 +70,8 @@ func TestOptionStoreErrors(t *testing.T) {
opts := loggers.Options{ opts := loggers.Options{
StoreErrors: true, StoreErrors: true,
Stderr: &sb, StdErr: &sb,
Stdout: &sb, StdOut: &sb,
} }
l := loggers.New(opts) l := loggers.New(opts)
@ -131,8 +131,8 @@ func TestReset(t *testing.T) {
opts := loggers.Options{ opts := loggers.Options{
StoreErrors: true, StoreErrors: true,
DistinctLevel: logg.LevelWarn, DistinctLevel: logg.LevelWarn,
Stdout: io.Discard, StdOut: io.Discard,
Stderr: io.Discard, StdErr: io.Discard,
} }
l := loggers.New(opts) l := loggers.New(opts)

8
deps/deps.go vendored
View file

@ -405,9 +405,11 @@ type DepsCfg struct {
// The logging level to use. // The logging level to use.
LogLevel logg.Level LogLevel logg.Level
// Where to write the logs. // Logging output.
// Currently we typically write everything to stdout. StdErr io.Writer
LogOut io.Writer
// The console output.
StdOut io.Writer
// The file systems to use // The file systems to use
Fs *hugofs.Fs Fs *hugofs.Fs

View file

@ -660,8 +660,8 @@ func (s *IntegrationTestBuilder) initBuilder() error {
logger := loggers.New( logger := loggers.New(
loggers.Options{ loggers.Options{
Stdout: w, StdOut: w,
Stderr: w, StdErr: w,
Level: s.Cfg.LogLevel, Level: s.Cfg.LogLevel,
DistinctLevel: logg.LevelWarn, DistinctLevel: logg.LevelWarn,
}, },
@ -685,7 +685,7 @@ func (s *IntegrationTestBuilder) initBuilder() error {
s.Assert(err, qt.IsNil) s.Assert(err, qt.IsNil)
depsCfg := deps.DepsCfg{Configs: res, Fs: fs, LogLevel: logger.Level(), LogOut: logger.Out()} depsCfg := deps.DepsCfg{Configs: res, Fs: fs, LogLevel: logger.Level(), StdErr: logger.StdErr()}
sites, err := NewHugoSites(depsCfg) sites, err := NewHugoSites(depsCfg)
if err != nil { if err != nil {
initErr = err initErr = err

View file

@ -145,8 +145,11 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
if cfg.Configs.Base.PanicOnWarning { if cfg.Configs.Base.PanicOnWarning {
logHookLast = loggers.PanicOnWarningHook logHookLast = loggers.PanicOnWarningHook
} }
if cfg.LogOut == nil { if cfg.StdOut == nil {
cfg.LogOut = os.Stdout cfg.StdOut = os.Stdout
}
if cfg.StdErr == nil {
cfg.StdErr = os.Stderr
} }
if cfg.LogLevel == 0 { if cfg.LogLevel == 0 {
cfg.LogLevel = logg.LevelWarn cfg.LogLevel = logg.LevelWarn
@ -156,8 +159,8 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
Level: cfg.LogLevel, Level: cfg.LogLevel,
DistinctLevel: logg.LevelWarn, // This will drop duplicate log warning and errors. DistinctLevel: logg.LevelWarn, // This will drop duplicate log warning and errors.
HandlerPost: logHookLast, HandlerPost: logHookLast,
Stdout: cfg.LogOut, StdOut: cfg.StdOut,
Stderr: cfg.LogOut, StdErr: cfg.StdErr,
StoreErrors: conf.Watching(), StoreErrors: conf.Watching(),
SuppressStatements: conf.IgnoredLogs(), SuppressStatements: conf.IgnoredLogs(),
} }

View file

@ -365,7 +365,7 @@ func (c *Client) Get(args ...string) error {
} }
func (c *Client) get(args ...string) error { func (c *Client) get(args ...string) error {
if err := c.runGo(context.Background(), c.logger.Out(), append([]string{"get"}, args...)...); err != nil { if err := c.runGo(context.Background(), c.logger.StdOut(), append([]string{"get"}, args...)...); err != nil {
return fmt.Errorf("failed to get %q: %w", args, err) return fmt.Errorf("failed to get %q: %w", args, err)
} }
return nil return nil
@ -375,7 +375,7 @@ func (c *Client) get(args ...string) error {
// If path is empty, Go will try to guess. // If path is empty, Go will try to guess.
// If this succeeds, this project will be marked as Go Module. // If this succeeds, this project will be marked as Go Module.
func (c *Client) Init(path string) error { func (c *Client) Init(path string) error {
err := c.runGo(context.Background(), c.logger.Out(), "mod", "init", path) err := c.runGo(context.Background(), c.logger.StdOut(), "mod", "init", path)
if err != nil { if err != nil {
return fmt.Errorf("failed to init modules: %w", err) return fmt.Errorf("failed to init modules: %w", err)
} }

View file

@ -1,13 +1,13 @@
# Test deprecation logging. # Test deprecation logging.
hugo -e info --logLevel info hugo -e info --logLevel info
stdout 'INFO deprecated: item was deprecated in Hugo' stderr 'INFO deprecated: item was deprecated in Hugo'
hugo -e warn --logLevel warn hugo -e warn --logLevel warn
stdout 'WARN deprecated: item was deprecated in Hugo' stderr 'WARN deprecated: item was deprecated in Hugo'
! hugo -e error --logLevel warn ! hugo -e error --logLevel warn
stdout 'ERROR deprecated: item was deprecated in Hugo' stderr 'ERROR deprecated: item was deprecated in Hugo'
-- hugo.toml -- -- hugo.toml --
baseURL = "https://example.com/" baseURL = "https://example.com/"

View file

@ -4,3 +4,4 @@ hugo
-- config/_default/hugo.toml -- -- config/_default/hugo.toml --
baseURL = "https://example.com/" baseURL = "https://example.com/"
disableKinds = ["RSS", "page", "sitemap", "robotsTXT", "404", "taxonomy", "term", "home"]

View file

@ -1,6 +1,6 @@
hugo --printPathWarnings hugo --printPathWarnings
stdout 'Duplicate' stderr 'Duplicate'
-- hugo.toml -- -- hugo.toml --
-- assets/css/styles.css -- -- assets/css/styles.css --

View file

@ -1,6 +1,6 @@
hugo --printPathWarnings hugo --printPathWarnings
stdout 'Duplicate target paths: .index.html \(2\)' stderr 'Duplicate target paths: .index.html \(2\)'
-- hugo.toml -- -- hugo.toml --
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "404", "section"] disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "404", "section"]

View file

@ -1,6 +1,6 @@
hugo --printUnusedTemplates hugo --printUnusedTemplates
stdout 'Template _default/list.html is unused' stderr 'Template _default/list.html is unused'
-- hugo.toml -- -- hugo.toml --
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "404", "section", "page"] disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "404", "section", "page"]

View file

@ -0,0 +1,13 @@
# Issue #13074
hugo
stderr 'warning'
! stdout 'warning'
-- hugo.toml --
baseURL = "http://example.org/"
disableKinds = ["RSS", "page", "sitemap", "robotsTXT", "404", "taxonomy", "term"]
-- layouts/index.html --
Home
{{ warnf "This is a warning" }}