diff --git a/cache/filecache/integration_test.go b/cache/filecache/integration_test.go index a59ea048d..a8a45988e 100644 --- a/cache/filecache/integration_test.go +++ b/cache/filecache/integration_test.go @@ -16,11 +16,10 @@ package filecache_test import ( "path/filepath" - jww "github.com/spf13/jwalterweatherman" - "testing" "time" + "github.com/bep/logg" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/hugolib" @@ -80,7 +79,7 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA ` b := hugolib.NewIntegrationTestBuilder( - hugolib.IntegrationTestConfig{T: t, TxtarString: files, Running: true, RunGC: true, NeedsOsFS: true, LogLevel: jww.LevelInfo}, + hugolib.IntegrationTestConfig{T: t, TxtarString: files, Running: true, RunGC: true, NeedsOsFS: true, LogLevel: logg.LevelInfo}, ).Build() b.Assert(b.GCCount, qt.Equals, 0) diff --git a/commands/commandeer.go b/commands/commandeer.go index f7b711973..7322a210a 100644 --- a/commands/commandeer.go +++ b/commands/commandeer.go @@ -28,12 +28,11 @@ import ( "syscall" "time" - jww "github.com/spf13/jwalterweatherman" - "go.uber.org/automaxprocs/maxprocs" "github.com/bep/clock" "github.com/bep/lazycache" + "github.com/bep/logg" "github.com/bep/overlayfs" "github.com/bep/simplecobra" @@ -114,7 +113,6 @@ type rootCommand struct { baseURL string gc bool poll string - panicOnWarning bool forceSyncStatic bool printPathWarnings bool printUnusedTemplates bool @@ -308,7 +306,7 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo func (r *rootCommand) HugFromConfig(conf *commonConfig) (*hugolib.HugoSites, error) { h, _, err := r.hugoSites.GetOrCreate(r.configVersionID.Load(), func(key int32) (*hugolib.HugoSites, error) { - depsCfg := deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, Logger: r.logger} + depsCfg := deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, LogOut: r.logger.Out(), LogLevel: r.logger.Level()} return hugolib.NewHugoSites(depsCfg) }) return h, err @@ -320,7 +318,7 @@ func (r *rootCommand) Hugo(cfg config.Provider) (*hugolib.HugoSites, error) { if err != nil { return nil, err } - depsCfg := deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, Logger: r.logger} + depsCfg := deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, LogOut: r.logger.Out(), LogLevel: r.logger.Level()} return hugolib.NewHugoSites(depsCfg) }) return h, err @@ -410,7 +408,6 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error { return err } - loggers.PanicOnWarning.Store(r.panicOnWarning) r.commonConfigs = lazycache.New[int32, *commonConfig](lazycache.Options{MaxEntries: 5}) r.hugoSites = lazycache.New[int32, *hugolib.HugoSites](lazycache.Options{MaxEntries: 5}) @@ -418,43 +415,48 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error { } func (r *rootCommand) createLogger(running bool) (loggers.Logger, error) { - var ( - outHandle = r.Out - stdoutThreshold = jww.LevelWarn - ) - - if r.verbose { - helpers.Deprecated("--verbose", "use --logLevel info", false) - stdoutThreshold = jww.LevelInfo - } - - if r.debug { - helpers.Deprecated("--debug", "use --logLevel debug", false) - stdoutThreshold = jww.LevelDebug - } + level := logg.LevelWarn if r.logLevel != "" { switch strings.ToLower(r.logLevel) { case "debug": - stdoutThreshold = jww.LevelDebug + level = logg.LevelDebug case "info": - stdoutThreshold = jww.LevelInfo + level = logg.LevelInfo case "warn", "warning": - stdoutThreshold = jww.LevelWarn + level = logg.LevelWarn case "error": - stdoutThreshold = jww.LevelError + level = logg.LevelError default: return nil, fmt.Errorf("invalid log level: %q, must be one of debug, warn, info or error", r.logLevel) } + } else { + if r.verbose { + helpers.Deprecated("--verbose", "use --logLevel info", false) + level = logg.LevelInfo + } + + if r.debug { + helpers.Deprecated("--debug", "use --logLevel debug", false) + level = logg.LevelDebug + } } - loggers.InitGlobalLogger(stdoutThreshold, jww.LevelWarn, outHandle, io.Discard) - helpers.InitLoggers() - return loggers.NewLogger(stdoutThreshold, jww.LevelWarn, outHandle, io.Discard, running), nil + optsLogger := loggers.Options{ + Distinct: true, + Level: level, + Stdout: r.Out, + Stderr: r.Out, + StoreErrors: running, + } + + return loggers.New(optsLogger), nil + } func (r *rootCommand) Reset() { r.logger.Reset() + loggers.Log().Reset() } // IsTestRun reports whether the command is running as a test. @@ -530,7 +532,7 @@ func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) { cmd.Flags().StringP("layoutDir", "l", "", "filesystem path to layout directory") cmd.Flags().BoolVar(&r.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build") cmd.Flags().StringVar(&r.poll, "poll", "", "set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes") - cmd.Flags().BoolVar(&r.panicOnWarning, "panicOnWarning", false, "panic on first WARNING log") + cmd.Flags().Bool("panicOnWarning", false, "panic on first WARNING log") cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions") cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics") cmd.Flags().BoolVar(&r.forceSyncStatic, "forceSyncStatic", false, "copy all files when static is changed.") diff --git a/commands/deploy.go b/commands/deploy.go index 8dae4bd88..82127adf4 100644 --- a/commands/deploy.go +++ b/commands/deploy.go @@ -52,7 +52,7 @@ documentation. if err != nil { return err } - deployer, err := deploy.New(h.Configs.GetFirstLanguageConfig(), h.PathSpec.PublishFs) + deployer, err := deploy.New(h.Configs.GetFirstLanguageConfig(), h.Log, h.PathSpec.PublishFs) if err != nil { return err } diff --git a/commands/hugobuilder.go b/commands/hugobuilder.go index 95dbb1ca8..60e558c70 100644 --- a/commands/hugobuilder.go +++ b/commands/hugobuilder.go @@ -26,11 +26,13 @@ import ( "sync" "time" + "github.com/bep/logg" "github.com/bep/simplecobra" "github.com/fsnotify/fsnotify" "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/htime" "github.com/gohugoio/hugo/common/hugo" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/terminal" "github.com/gohugoio/hugo/common/types" @@ -68,7 +70,6 @@ type hugoBuilder struct { onConfigLoaded func(reloaded bool) error fastRenderMode bool - buildWatch bool showErrorInBrowser bool errState hugoBuilderErrState @@ -131,7 +132,7 @@ func (e *hugoBuilderErrState) wasErr() bool { } func (c *hugoBuilder) errCount() int { - return int(c.r.logger.LogCounters().ErrorCounter.Count()) + return c.r.logger.LoggCount(logg.LevelError) + loggers.Log().LoggCount(logg.LevelError) } // getDirList provides NewWatcher() with a list of directories to watch for changes. @@ -363,7 +364,7 @@ func (c *hugoBuilder) newWatcher(pollIntervalStr string, dirList ...string) (*wa configFiles = conf.configs.LoadingInfo.ConfigFiles }) - c.r.logger.Println("Watching for config changes in", strings.Join(configFiles, ", ")) + c.r.Println("Watching for config changes in", strings.Join(configFiles, ", ")) for _, configFile := range configFiles { watcher.Add(configFile) configSet[configFile] = true @@ -461,6 +462,7 @@ func (c *hugoBuilder) copyStatic() (map[string]uint64, error) { } func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) { + infol := c.r.logger.InfoCommand("copy static") publishDir := helpers.FilePathSeparator if sourceFs.PublishFolder != "" { @@ -484,13 +486,13 @@ func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint syncer.SrcFs = fs if syncer.Delete { - c.r.logger.Infoln("removing all files from destination that don't exist in static dirs") + infol.Logf("removing all files from destination that don't exist in static dirs") syncer.DeleteFilter = func(f os.FileInfo) bool { return f.IsDir() && strings.HasPrefix(f.Name(), ".") } } - c.r.logger.Infoln("syncing static files to", publishDir) + infol.Logf("syncing static files to %s", publishDir) // because we are using a baseFs (to get the union right). // set sync src to root @@ -545,14 +547,13 @@ func (c *hugoBuilder) fullBuild(noBuildLock bool) error { langCount map[string]uint64 ) - if !c.r.quiet { - fmt.Println("Start building sites … ") - fmt.Println(hugo.BuildVersionString()) - if terminal.IsTerminal(os.Stdout) { - defer func() { - fmt.Print(showCursor + clearLine) - }() - } + c.r.logger.Println("Start building sites … ") + c.r.logger.Println(hugo.BuildVersionString()) + c.r.logger.Println() + if terminal.IsTerminal(os.Stdout) { + defer func() { + fmt.Print(showCursor + clearLine) + }() } copyStaticFunc := func() error { diff --git a/commands/import.go b/commands/import.go index 30ada15f8..f2c56a9a1 100644 --- a/commands/import.go +++ b/commands/import.go @@ -19,12 +19,11 @@ import ( "errors" "fmt" "io" + "log" "os" "path/filepath" "regexp" - jww "github.com/spf13/jwalterweatherman" - "strconv" "strings" "time" @@ -299,7 +298,7 @@ func (c *importCommand) convertJekyllMetaData(m any, postName string, postDate t } func (c *importCommand) convertJekyllPost(path, relPath, targetDir string, draft bool) error { - jww.TRACE.Println("Converting", path) + log.Println("Converting", path) filename := filepath.Base(path) postDate, postName, err := c.parseJekyllFilename(filename) @@ -308,7 +307,7 @@ func (c *importCommand) convertJekyllPost(path, relPath, targetDir string, draft return nil } - jww.TRACE.Println(filename, postDate, postName) + log.Println(filename, postDate, postName) targetFile := filepath.Join(targetDir, relPath) targetParentDir := filepath.Dir(targetFile) @@ -367,7 +366,7 @@ func (c *importCommand) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyl if _, ok := jekyllPostDirs[entry.Name()]; !ok { err = hugio.CopyDir(fs, sfp, dfp, nil) if err != nil { - jww.ERROR.Println(err) + c.r.logger.Errorln(err) } } } @@ -388,7 +387,7 @@ func (c *importCommand) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyl if !isExcept && entry.Name()[0] != '.' && entry.Name()[0] != '_' { err = hugio.CopyFile(fs, sfp, dfp) if err != nil { - jww.ERROR.Println(err) + c.r.logger.Errorln(err) } } } diff --git a/commands/server.go b/commands/server.go index 80036618c..7e6ec5b13 100644 --- a/commands/server.go +++ b/commands/server.go @@ -67,7 +67,6 @@ import ( ) var ( - logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `) logDuplicateTemplateExecuteRe = regexp.MustCompile(`: template: .*?:\d+:\d+: executing ".*?"`) logDuplicateTemplateParseRe = regexp.MustCompile(`: template: .*?:\d+:\d*`) ) @@ -106,9 +105,7 @@ func newServerCommand() *serverCommand { // Flags. var uninstall bool - var c *serverCommand - - c = &serverCommand{ + c := &serverCommand{ quit: make(chan bool), commands: []simplecobra.Commander{ &simpleCommand{ @@ -654,8 +651,8 @@ func (c *serverCommand) getErrorWithContext() any { m := make(map[string]any) - //xwm["Error"] = errors.New(cleanErrorLog(removeErrorPrefixFromLog(c.r.logger.Errors()))) - m["Error"] = errors.New(cleanErrorLog(removeErrorPrefixFromLog(c.r.logger.Errors()))) + m["Error"] = cleanErrorLog(c.r.logger.Errors()) + m["Version"] = hugo.BuildVersionString() ferrors := herrors.UnwrapFileErrorsWithErrorContext(c.errState.buildErr()) m["Files"] = ferrors @@ -861,6 +858,9 @@ func (c *serverCommand) serve() error { return err } + // We need the server to share the same logger as the Hugo build (for error counts etc.) + c.r.logger = h.Log + if isMultiHost { for _, l := range conf.configs.ConfigLangs() { baseURLs = append(baseURLs, l.BaseURL().String()) @@ -1066,8 +1066,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error { } }) - // prevent spamming the log on changes - logger := helpers.NewDistinctErrorLogger() + logger := s.c.r.logger for _, ev := range staticEvents { // Due to our approach of layering both directories and the content's rendered output @@ -1206,10 +1205,6 @@ func pickOneWriteOrCreatePath(events []fsnotify.Event) string { return name } -func removeErrorPrefixFromLog(content string) string { - return logErrorRe.ReplaceAllLiteralString(content, "") -} - func formatByteCount(b uint64) string { const unit = 1000 if b < unit { diff --git a/commands/xcommand_template.go b/commands/xcommand_template.go deleted file mode 100644 index eeb9409a0..000000000 --- a/commands/xcommand_template.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package commands - -import ( - "context" - "fmt" - - "github.com/bep/simplecobra" - "github.com/spf13/cobra" -) - -func newSimpleTemplateCommand() simplecobra.Commander { - return &simpleCommand{ - name: "template", - run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { - - return nil - }, - withc: func(cmd *cobra.Command, r *rootCommand) { - - }, - } - -} - -func newTemplateCommand() *templateCommand { - return &templateCommand{ - commands: []simplecobra.Commander{}, - } - -} - -type templateCommand struct { - r *rootCommand - - commands []simplecobra.Commander -} - -func (c *templateCommand) Commands() []simplecobra.Commander { - return c.commands -} - -func (c *templateCommand) Name() string { - return "template" -} - -func (c *templateCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { - conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, nil)) - if err != nil { - return err - } - fmt.Println("templateCommand.Run", conf) - - return nil -} - -func (c *templateCommand) Init(cd *simplecobra.Commandeer) error { - cmd := cd.CobraCommand - cmd.Short = "Print the site configuration" - cmd.Long = `Print the site configuration, both default and custom settings.` - return nil -} - -func (c *templateCommand) PreRun(cd, runner *simplecobra.Commandeer) error { - c.r = cd.Root.Command.(*rootCommand) - return nil -} diff --git a/common/loggers/handlerdefault.go b/common/loggers/handlerdefault.go new file mode 100644 index 000000000..28b85ed22 --- /dev/null +++ b/common/loggers/handlerdefault.go @@ -0,0 +1,106 @@ +// Copyright 2023 The Hugo Authors. All rights reserved. +// Some functions in this file (see comments) is based on the Go source code, +// copyright The Go Authors and governed by a BSD-style license. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// package loggers contains some basic logging setup. +package loggers + +import ( + "fmt" + "io" + "strings" + "sync" + + "github.com/bep/logg" + + "github.com/fatih/color" +) + +var bold = color.New(color.Bold) + +// levelColor mapping. +var levelColor = [...]*color.Color{ + logg.LevelDebug: color.New(color.FgWhite), + logg.LevelInfo: color.New(color.FgBlue), + logg.LevelWarn: color.New(color.FgYellow), + logg.LevelError: color.New(color.FgRed), +} + +// levelString mapping. +var levelString = [...]string{ + logg.LevelDebug: "DEBUG", + logg.LevelInfo: "INFO ", + logg.LevelWarn: "WARN ", + logg.LevelError: "ERROR", +} + +// newDefaultHandler handler. +func newDefaultHandler(outWriter, errWriter io.Writer) logg.Handler { + return &defaultHandler{ + outWriter: outWriter, + errWriter: errWriter, + Padding: 0, + } +} + +// Default Handler implementation. +// Based on https://github.com/apex/log/blob/master/handlers/cli/cli.go +type defaultHandler struct { + mu sync.Mutex + outWriter io.Writer // Defaults to os.Stdout. + errWriter io.Writer // Defaults to os.Stderr. + + Padding int +} + +// HandleLog implements logg.Handler. +func (h *defaultHandler) HandleLog(e *logg.Entry) error { + color := levelColor[e.Level] + level := levelString[e.Level] + + h.mu.Lock() + defer h.mu.Unlock() + + var w io.Writer + if e.Level > logg.LevelInfo { + w = h.errWriter + } else { + w = h.outWriter + } + + var prefix string + for _, field := range e.Fields { + if field.Name == FieldNameCmd { + prefix = fmt.Sprint(field.Value) + break + } + } + + if prefix != "" { + prefix = prefix + ": " + } + + color.Fprintf(w, "%s %s%s", bold.Sprintf("%*s", h.Padding+1, level), color.Sprint(prefix), e.Message) + + for _, field := range e.Fields { + if strings.HasPrefix(field.Name, reservedFieldNamePrefix) { + continue + } + fmt.Fprintf(w, " %s %v", color.Sprint(field.Name), field.Value) + } + + fmt.Fprintln(w) + + return nil +} diff --git a/common/loggers/handlersmisc.go b/common/loggers/handlersmisc.go new file mode 100644 index 000000000..5c9d6c091 --- /dev/null +++ b/common/loggers/handlersmisc.go @@ -0,0 +1,158 @@ +// Copyright 2023 The Hugo Authors. All rights reserved. +// Some functions in this file (see comments) is based on the Go source code, +// copyright The Go Authors and governed by a BSD-style license. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package loggers + +import ( + "fmt" + "strings" + "sync" + + "github.com/bep/logg" + "github.com/gohugoio/hugo/identity" +) + +// PanicOnWarningHook panics on warnings. +var PanicOnWarningHook = func(e *logg.Entry) error { + if e.Level != logg.LevelWarn { + return nil + } + panic(e.Message) +} + +func newLogLevelCounter() *logLevelCounter { + return &logLevelCounter{ + counters: make(map[logg.Level]int), + } +} + +func newLogOnceHandler(threshold logg.Level) *logOnceHandler { + return &logOnceHandler{ + threshold: threshold, + seen: make(map[uint64]bool), + } +} + +func newStopHandler(h ...logg.Handler) *stopHandler { + return &stopHandler{ + handlers: h, + } +} + +func newSuppressStatementsHandler(statements map[string]bool) *suppressStatementsHandler { + return &suppressStatementsHandler{ + statements: statements, + } +} + +type logLevelCounter struct { + mu sync.RWMutex + counters map[logg.Level]int +} + +func (h *logLevelCounter) HandleLog(e *logg.Entry) error { + h.mu.Lock() + defer h.mu.Unlock() + h.counters[e.Level]++ + return nil +} + +var stopError = fmt.Errorf("stop") + +type logOnceHandler struct { + threshold logg.Level + mu sync.Mutex + seen map[uint64]bool +} + +func (h *logOnceHandler) HandleLog(e *logg.Entry) error { + if e.Level < h.threshold { + // We typically only want to enable this for warnings and above. + // The common use case is that many go routines may log the same error. + return nil + } + h.mu.Lock() + defer h.mu.Unlock() + hash := identity.HashUint64(e.Level, e.Message, e.Fields) + if h.seen[hash] { + return stopError + } + h.seen[hash] = true + return nil +} + +func (h *logOnceHandler) reset() { + h.mu.Lock() + defer h.mu.Unlock() + h.seen = make(map[uint64]bool) +} + +type stopHandler struct { + handlers []logg.Handler +} + +// HandleLog implements logg.Handler. +func (h *stopHandler) HandleLog(e *logg.Entry) error { + for _, handler := range h.handlers { + if err := handler.HandleLog(e); err != nil { + if err == stopError { + return nil + } + return err + } + } + return nil +} + +type suppressStatementsHandler struct { + statements map[string]bool +} + +func (h *suppressStatementsHandler) HandleLog(e *logg.Entry) error { + for _, field := range e.Fields { + if field.Name == FieldNameStatementID { + if h.statements[field.Value.(string)] { + return stopError + } + } + } + return nil +} + +// replacer creates a new log handler that does string replacement in log messages. +func replacer(repl *strings.Replacer) logg.Handler { + return logg.HandlerFunc(func(e *logg.Entry) error { + e.Message = repl.Replace(e.Message) + for i, field := range e.Fields { + if s, ok := field.Value.(string); ok { + e.Fields[i].Value = repl.Replace(s) + } + } + return nil + }) +} + +// whiteSpaceTrimmer creates a new log handler that trims whitespace from log messages and string fields. +func whiteSpaceTrimmer() logg.Handler { + return logg.HandlerFunc(func(e *logg.Entry) error { + e.Message = strings.TrimSpace(e.Message) + for i, field := range e.Fields { + if s, ok := field.Value.(string); ok { + e.Fields[i].Value = strings.TrimSpace(s) + } + } + return nil + }) +} diff --git a/common/loggers/handlerterminal.go b/common/loggers/handlerterminal.go new file mode 100644 index 000000000..e3d377bbf --- /dev/null +++ b/common/loggers/handlerterminal.go @@ -0,0 +1,90 @@ +// Copyright 2023 The Hugo Authors. All rights reserved. +// Some functions in this file (see comments) is based on the Go source code, +// copyright The Go Authors and governed by a BSD-style license. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package loggers + +import ( + "fmt" + "io" + "strings" + "sync" + + "github.com/bep/logg" +) + +// newNoColoursHandler creates a new NoColoursHandler +func newNoColoursHandler(outWriter, errWriter io.Writer, noLevelPrefix bool, predicate func(*logg.Entry) bool) *noColoursHandler { + if predicate == nil { + predicate = func(e *logg.Entry) bool { return true } + } + return &noColoursHandler{ + noLevelPrefix: noLevelPrefix, + outWriter: outWriter, + errWriter: errWriter, + predicate: predicate, + } +} + +type noColoursHandler struct { + mu sync.Mutex + outWriter io.Writer // Defaults to os.Stdout. + errWriter io.Writer // Defaults to os.Stderr. + predicate func(*logg.Entry) bool + noLevelPrefix bool +} + +func (h *noColoursHandler) HandleLog(e *logg.Entry) error { + if !h.predicate(e) { + return nil + } + h.mu.Lock() + defer h.mu.Unlock() + + var w io.Writer + if e.Level > logg.LevelInfo { + w = h.errWriter + } else { + w = h.outWriter + } + + var prefix string + for _, field := range e.Fields { + if field.Name == FieldNameCmd { + prefix = fmt.Sprint(field.Value) + break + } + } + + if prefix != "" { + prefix = prefix + ": " + } + + if h.noLevelPrefix { + fmt.Fprintf(w, "%s%s", prefix, e.Message) + } else { + fmt.Fprintf(w, "%s %s%s", levelString[e.Level], prefix, e.Message) + } + + for _, field := range e.Fields { + if strings.HasPrefix(field.Name, reservedFieldNamePrefix) { + continue + } + fmt.Fprintf(w, " %s %q", field.Name, field.Value) + + } + fmt.Fprintln(w) + + return nil +} diff --git a/common/loggers/ignorableLogger.go b/common/loggers/ignorableLogger.go deleted file mode 100644 index c8aba560e..000000000 --- a/common/loggers/ignorableLogger.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2020 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package loggers - -import ( - "fmt" -) - -// IgnorableLogger is a logger that ignores certain log statements. -type IgnorableLogger interface { - Logger - Errorsf(statementID, format string, v ...any) - Apply(logger Logger) IgnorableLogger -} - -type ignorableLogger struct { - Logger - statements map[string]bool -} - -// NewIgnorableLogger wraps the given logger and ignores the log statement IDs given. -func NewIgnorableLogger(logger Logger, statements map[string]bool) IgnorableLogger { - if statements == nil { - statements = make(map[string]bool) - } - return ignorableLogger{ - Logger: logger, - statements: statements, - } -} - -// Errorsf logs statementID as an ERROR if not configured as ignoreable. -func (l ignorableLogger) Errorsf(statementID, format string, v ...any) { - if l.statements[statementID] { - // Ignore. - return - } - ignoreMsg := fmt.Sprintf(` -If you feel that this should not be logged as an ERROR, you can ignore it by adding this to your site config: -ignoreErrors = [%q]`, statementID) - - format += ignoreMsg - - l.Errorf(format, v...) -} - -func (l ignorableLogger) Apply(logger Logger) IgnorableLogger { - return ignorableLogger{ - Logger: logger, - statements: l.statements, - } -} diff --git a/common/loggers/logger.go b/common/loggers/logger.go new file mode 100644 index 000000000..85c75ef98 --- /dev/null +++ b/common/loggers/logger.go @@ -0,0 +1,303 @@ +// Copyright 2023 The Hugo Authors. All rights reserved. +// Some functions in this file (see comments) is based on the Go source code, +// copyright The Go Authors and governed by a BSD-style license. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package loggers + +import ( + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/bep/logg" + "github.com/bep/logg/handlers/multi" + "github.com/gohugoio/hugo/common/terminal" +) + +var ( + reservedFieldNamePrefix = "__h_field_" + // FieldNameCmd is the name of the field that holds the command name. + FieldNameCmd = reservedFieldNamePrefix + "_cmd" + // Used to suppress statements. + FieldNameStatementID = reservedFieldNamePrefix + "__h_field_statement_id" +) + +// Options defines options for the logger. +type Options struct { + Level logg.Level + Stdout io.Writer + Stderr io.Writer + Distinct bool + StoreErrors bool + HandlerPost func(e *logg.Entry) error + SuppresssStatements map[string]bool +} + +// New creates a new logger with the given options. +func New(opts Options) Logger { + if opts.Stdout == nil { + opts.Stdout = os.Stdout + } + if opts.Stderr == nil { + opts.Stderr = os.Stdout + } + if opts.Level == 0 { + opts.Level = logg.LevelWarn + } + + var logHandler logg.Handler + if terminal.PrintANSIColors(os.Stdout) { + logHandler = newDefaultHandler(opts.Stdout, opts.Stderr) + } else { + logHandler = newNoColoursHandler(opts.Stdout, opts.Stderr, false, nil) + } + + errorsw := &strings.Builder{} + logCounters := newLogLevelCounter() + handlers := []logg.Handler{ + whiteSpaceTrimmer(), + logHandler, + logCounters, + } + + if opts.HandlerPost != nil { + var hookHandler logg.HandlerFunc = func(e *logg.Entry) error { + opts.HandlerPost(e) + return nil + } + handlers = append(handlers, hookHandler) + } + + if opts.StoreErrors { + h := newNoColoursHandler(io.Discard, errorsw, true, func(e *logg.Entry) bool { + return e.Level >= logg.LevelError + }) + + handlers = append(handlers, h) + } + + logHandler = multi.New(handlers...) + + var logOnce *logOnceHandler + if opts.Distinct { + logOnce = newLogOnceHandler(logg.LevelWarn) + logHandler = newStopHandler(logOnce, logHandler) + } + + if opts.SuppresssStatements != nil && len(opts.SuppresssStatements) > 0 { + logHandler = newStopHandler(newSuppressStatementsHandler(opts.SuppresssStatements), logHandler) + } + + logger := logg.New( + logg.Options{ + Level: opts.Level, + Handler: logHandler, + }, + ) + + l := logger.WithLevel(opts.Level) + + reset := func() { + logCounters.mu.Lock() + defer logCounters.mu.Unlock() + logCounters.counters = make(map[logg.Level]int) + errorsw.Reset() + if logOnce != nil { + logOnce.reset() + } + } + + return &logAdapter{ + logCounters: logCounters, + errors: errorsw, + reset: reset, + out: opts.Stdout, + level: opts.Level, + logger: logger, + debugl: l.WithLevel(logg.LevelDebug), + infol: l.WithLevel(logg.LevelInfo), + warnl: l.WithLevel(logg.LevelWarn), + errorl: l.WithLevel(logg.LevelError), + } +} + +// NewDefault creates a new logger with the default options. +func NewDefault() Logger { + opts := Options{ + Distinct: true, + Level: logg.LevelWarn, + Stdout: os.Stdout, + Stderr: os.Stdout, + } + return New(opts) +} + +func LevelLoggerToWriter(l logg.LevelLogger) io.Writer { + return logWriter{l: l} +} + +type Logger interface { + Debugf(format string, v ...any) + Debugln(v ...any) + Error() logg.LevelLogger + Errorf(format string, v ...any) + Errorln(v ...any) + Errors() string + Errorsf(id, format string, v ...any) + Info() logg.LevelLogger + InfoCommand(command string) logg.LevelLogger + Infof(format string, v ...any) + Infoln(v ...any) + Level() logg.Level + LoggCount(logg.Level) int + Logger() logg.Logger + Out() io.Writer + Printf(format string, v ...any) + Println(v ...any) + PrintTimerIfDelayed(start time.Time, name string) + Reset() + Warn() logg.LevelLogger + WarnCommand(command string) logg.LevelLogger + Warnf(format string, v ...any) + Warnln(v ...any) +} + +type logAdapter struct { + logCounters *logLevelCounter + errors *strings.Builder + reset func() + out io.Writer + level logg.Level + logger logg.Logger + debugl logg.LevelLogger + infol logg.LevelLogger + warnl logg.LevelLogger + errorl logg.LevelLogger +} + +func (l *logAdapter) Debugf(format string, v ...any) { + l.debugl.Logf(format, v...) +} + +func (l *logAdapter) Debugln(v ...any) { + l.debugl.Logf(l.sprint(v...)) +} + +func (l *logAdapter) Info() logg.LevelLogger { + return l.infol +} + +func (l *logAdapter) InfoCommand(command string) logg.LevelLogger { + return l.infol.WithField(FieldNameCmd, command) +} + +func (l *logAdapter) Infof(format string, v ...any) { + l.infol.Logf(format, v...) +} + +func (l *logAdapter) Infoln(v ...any) { + l.infol.Logf(l.sprint(v...)) +} + +func (l *logAdapter) Level() logg.Level { + return l.level +} + +func (l *logAdapter) LoggCount(level logg.Level) int { + l.logCounters.mu.RLock() + defer l.logCounters.mu.RUnlock() + return l.logCounters.counters[level] +} + +func (l *logAdapter) Logger() logg.Logger { + return l.logger +} + +func (l *logAdapter) Out() io.Writer { + return l.out +} + +// PrintTimerIfDelayed prints a time statement to the FEEDBACK logger +// if considerable time is spent. +func (l *logAdapter) PrintTimerIfDelayed(start time.Time, name string) { + elapsed := time.Since(start) + milli := int(1000 * elapsed.Seconds()) + if milli < 500 { + return + } + l.Printf("%s in %v ms", name, milli) +} + +func (l *logAdapter) Printf(format string, v ...any) { + fmt.Fprintf(l.out, format, v...) +} + +func (l *logAdapter) Println(v ...any) { + fmt.Fprintln(l.out, v...) +} + +func (l *logAdapter) Reset() { + l.reset() +} + +func (l *logAdapter) Warn() logg.LevelLogger { + return l.warnl +} + +func (l *logAdapter) Warnf(format string, v ...any) { + l.warnl.Logf(format, v...) +} + +func (l *logAdapter) WarnCommand(command string) logg.LevelLogger { + return l.warnl.WithField(FieldNameCmd, command) +} + +func (l *logAdapter) Warnln(v ...any) { + l.warnl.Logf(l.sprint(v...)) +} + +func (l *logAdapter) Error() logg.LevelLogger { + return l.errorl +} + +func (l *logAdapter) Errorf(format string, v ...any) { + l.errorl.Logf(format, v...) +} + +func (l *logAdapter) Errorln(v ...any) { + l.errorl.Logf(l.sprint(v...)) +} + +func (l *logAdapter) Errors() string { + return l.errors.String() +} + +func (l *logAdapter) Errorsf(id, format string, v ...any) { + l.errorl.WithField(FieldNameStatementID, id).Logf(format, v...) +} + +func (l *logAdapter) sprint(v ...any) string { + return strings.TrimRight(fmt.Sprintln(v...), "\n") +} + +type logWriter struct { + l logg.LevelLogger +} + +func (w logWriter) Write(p []byte) (n int, err error) { + w.l.Logf("%s", p) + return len(p), nil +} diff --git a/common/loggers/logger_test.go b/common/loggers/logger_test.go new file mode 100644 index 000000000..6aa540b0b --- /dev/null +++ b/common/loggers/logger_test.go @@ -0,0 +1,156 @@ +// Copyright 2023 The Hugo Authors. All rights reserved. +// Some functions in this file (see comments) is based on the Go source code, +// copyright The Go Authors and governed by a BSD-style license. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package loggers_test + +import ( + "io" + "strings" + "testing" + + "github.com/bep/logg" + qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/common/loggers" +) + +func TestLogDistinct(t *testing.T) { + c := qt.New(t) + + opts := loggers.Options{ + Distinct: true, + StoreErrors: true, + Stdout: io.Discard, + Stderr: io.Discard, + } + + l := loggers.New(opts) + + for i := 0; i < 10; i++ { + l.Errorln("error 1") + l.Errorln("error 2") + l.Warnln("warn 1") + } + c.Assert(strings.Count(l.Errors(), "error 1"), qt.Equals, 1) + c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 2) + c.Assert(l.LoggCount(logg.LevelWarn), qt.Equals, 1) +} + +func TestHookLast(t *testing.T) { + c := qt.New(t) + + opts := loggers.Options{ + HandlerPost: func(e *logg.Entry) error { + panic(e.Message) + }, + Stdout: io.Discard, + Stderr: io.Discard, + } + + l := loggers.New(opts) + + c.Assert(func() { l.Warnln("warn 1") }, qt.PanicMatches, "warn 1") +} + +func TestOptionStoreErrors(t *testing.T) { + c := qt.New(t) + + var sb strings.Builder + + opts := loggers.Options{ + StoreErrors: true, + Stderr: &sb, + Stdout: &sb, + } + + l := loggers.New(opts) + l.Errorln("error 1") + l.Errorln("error 2") + + errorsStr := l.Errors() + + c.Assert(errorsStr, qt.Contains, "error 1") + c.Assert(errorsStr, qt.Not(qt.Contains), "ERROR") + + c.Assert(sb.String(), qt.Contains, "error 1") + c.Assert(sb.String(), qt.Contains, "ERROR") + +} + +func TestLogCount(t *testing.T) { + c := qt.New(t) + + opts := loggers.Options{ + StoreErrors: true, + } + + l := loggers.New(opts) + l.Errorln("error 1") + l.Errorln("error 2") + l.Warnln("warn 1") + + c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 2) + c.Assert(l.LoggCount(logg.LevelWarn), qt.Equals, 1) + c.Assert(l.LoggCount(logg.LevelInfo), qt.Equals, 0) +} + +func TestSuppressStatements(t *testing.T) { + c := qt.New(t) + + opts := loggers.Options{ + StoreErrors: true, + SuppresssStatements: map[string]bool{ + "error-1": true, + }, + } + + l := loggers.New(opts) + l.Error().WithField(loggers.FieldNameStatementID, "error-1").Logf("error 1") + l.Errorln("error 2") + + errorsStr := l.Errors() + + c.Assert(errorsStr, qt.Not(qt.Contains), "error 1") + c.Assert(errorsStr, qt.Contains, "error 2") + c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 1) + +} + +func TestReset(t *testing.T) { + c := qt.New(t) + + opts := loggers.Options{ + StoreErrors: true, + Distinct: true, + Stdout: io.Discard, + Stderr: io.Discard, + } + + l := loggers.New(opts) + + for i := 0; i < 3; i++ { + l.Errorln("error 1") + l.Errorln("error 2") + l.Errorln("error 1") + c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 2) + + l.Reset() + + errorsStr := l.Errors() + + c.Assert(errorsStr, qt.Equals, "") + c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 0) + + } +} diff --git a/common/loggers/loggerglobal.go b/common/loggers/loggerglobal.go new file mode 100644 index 000000000..92b2469ba --- /dev/null +++ b/common/loggers/loggerglobal.go @@ -0,0 +1,53 @@ +// Copyright 2023 The Hugo Authors. All rights reserved. +// Some functions in this file (see comments) is based on the Go source code, +// copyright The Go Authors and governed by a BSD-style license. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package loggers + +import ( + "sync" + + "github.com/bep/logg" +) + +func InitGlobalLogger(panicOnWarnings bool) { + logMu.Lock() + defer logMu.Unlock() + var logHookLast func(e *logg.Entry) error + if panicOnWarnings { + logHookLast = PanicOnWarningHook + } + + log = New( + Options{ + Distinct: true, + HandlerPost: logHookLast, + }, + ) +} + +var logMu sync.Mutex + +func Log() Logger { + logMu.Lock() + defer logMu.Unlock() + return log +} + +// The global logger. +var log Logger + +func init() { + InitGlobalLogger(false) +} diff --git a/common/loggers/loggers.go b/common/loggers/loggers.go deleted file mode 100644 index fbbbca435..000000000 --- a/common/loggers/loggers.go +++ /dev/null @@ -1,355 +0,0 @@ -// Copyright 2020 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package loggers - -import ( - "bytes" - "fmt" - "io" - "log" - "os" - "regexp" - "runtime" - "sync/atomic" - "time" - - "github.com/gohugoio/hugo/common/terminal" - - jww "github.com/spf13/jwalterweatherman" -) - -var ( - // Counts ERROR logs to the global jww logger. - GlobalErrorCounter *jww.Counter - PanicOnWarning atomic.Bool -) - -func init() { - GlobalErrorCounter = &jww.Counter{} - jww.SetLogListeners(jww.LogCounter(GlobalErrorCounter, jww.LevelError)) -} - -func LoggerToWriterWithPrefix(logger *log.Logger, prefix string) io.Writer { - return prefixWriter{ - logger: logger, - prefix: prefix, - } -} - -type prefixWriter struct { - logger *log.Logger - prefix string -} - -func (w prefixWriter) Write(p []byte) (n int, err error) { - w.logger.Printf("%s: %s", w.prefix, p) - return len(p), nil -} - -type Logger interface { - Printf(format string, v ...any) - Println(v ...any) - PrintTimerIfDelayed(start time.Time, name string) - Debug() *log.Logger - Debugf(format string, v ...any) - Debugln(v ...any) - Info() *log.Logger - Infof(format string, v ...any) - Infoln(v ...any) - Warn() *log.Logger - Warnf(format string, v ...any) - Warnln(v ...any) - Error() *log.Logger - Errorf(format string, v ...any) - Errorln(v ...any) - Errors() string - - Out() io.Writer - - Reset() - - // Used in tests. - LogCounters() *LogCounters -} - -type LogCounters struct { - ErrorCounter *jww.Counter - WarnCounter *jww.Counter -} - -type logger struct { - *jww.Notepad - - // The writer that represents stdout. - // Will be io.Discard when in quiet mode. - out io.Writer - - logCounters *LogCounters - - // This is only set in server mode. - errors *bytes.Buffer -} - -func (l *logger) Printf(format string, v ...any) { - l.FEEDBACK.Printf(format, v...) -} - -func (l *logger) Println(v ...any) { - l.FEEDBACK.Println(v...) -} - -func (l *logger) Debug() *log.Logger { - return l.DEBUG -} - -func (l *logger) Debugf(format string, v ...any) { - l.DEBUG.Printf(format, v...) -} - -func (l *logger) Debugln(v ...any) { - l.DEBUG.Println(v...) -} - -func (l *logger) Infof(format string, v ...any) { - l.INFO.Printf(format, v...) -} - -func (l *logger) Infoln(v ...any) { - l.INFO.Println(v...) -} - -func (l *logger) Info() *log.Logger { - return l.INFO -} - -const panicOnWarningMessage = "Warning trapped. Remove the --panicOnWarning flag to continue." - -func (l *logger) Warnf(format string, v ...any) { - l.WARN.Printf(format, v...) - if PanicOnWarning.Load() { - panic(panicOnWarningMessage) - } -} - -func (l *logger) Warnln(v ...any) { - l.WARN.Println(v...) - if PanicOnWarning.Load() { - panic(panicOnWarningMessage) - } -} - -func (l *logger) Warn() *log.Logger { - return l.WARN -} - -func (l *logger) Errorf(format string, v ...any) { - l.ERROR.Printf(format, v...) -} - -func (l *logger) Errorln(v ...any) { - l.ERROR.Println(v...) -} - -func (l *logger) Error() *log.Logger { - return l.ERROR -} - -func (l *logger) LogCounters() *LogCounters { - return l.logCounters -} - -func (l *logger) Out() io.Writer { - return l.out -} - -// PrintTimerIfDelayed prints a time statement to the FEEDBACK logger -// if considerable time is spent. -func (l *logger) PrintTimerIfDelayed(start time.Time, name string) { - elapsed := time.Since(start) - milli := int(1000 * elapsed.Seconds()) - if milli < 500 { - return - } - l.Printf("%s in %v ms", name, milli) -} - -func (l *logger) PrintTimer(start time.Time, name string) { - elapsed := time.Since(start) - milli := int(1000 * elapsed.Seconds()) - l.Printf("%s in %v ms", name, milli) -} - -func (l *logger) Errors() string { - if l.errors == nil { - return "" - } - return ansiColorRe.ReplaceAllString(l.errors.String(), "") -} - -// Reset resets the logger's internal state. -func (l *logger) Reset() { - l.logCounters.ErrorCounter.Reset() - if l.errors != nil { - l.errors.Reset() - } -} - -// NewLogger creates a new Logger for the given thresholds -func NewLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) Logger { - return newLogger(stdoutThreshold, logThreshold, outHandle, logHandle, saveErrors) -} - -// NewDebugLogger is a convenience function to create a debug logger. -func NewDebugLogger() Logger { - return NewBasicLogger(jww.LevelDebug) -} - -// NewWarningLogger is a convenience function to create a warning logger. -func NewWarningLogger() Logger { - return NewBasicLogger(jww.LevelWarn) -} - -// NewInfoLogger is a convenience function to create a info logger. -func NewInfoLogger() Logger { - return NewBasicLogger(jww.LevelInfo) -} - -// NewErrorLogger is a convenience function to create an error logger. -func NewErrorLogger() Logger { - return NewBasicLogger(jww.LevelError) -} - -// NewBasicLogger creates a new basic logger writing to Stdout. -func NewBasicLogger(t jww.Threshold) Logger { - return newLogger(t, jww.LevelError, os.Stdout, io.Discard, false) -} - -// NewBasicLoggerForWriter creates a new basic logger writing to w. -func NewBasicLoggerForWriter(t jww.Threshold, w io.Writer) Logger { - return newLogger(t, jww.LevelError, w, io.Discard, false) -} - -// RemoveANSIColours removes all ANSI colours from the given string. -func RemoveANSIColours(s string) string { - return ansiColorRe.ReplaceAllString(s, "") -} - -var ( - ansiColorRe = regexp.MustCompile("(?s)\\033\\[\\d*(;\\d*)*m") - errorRe = regexp.MustCompile("^(ERROR|FATAL|WARN)") -) - -type ansiCleaner struct { - w io.Writer -} - -func (a ansiCleaner) Write(p []byte) (n int, err error) { - return a.w.Write(ansiColorRe.ReplaceAll(p, []byte(""))) -} - -type labelColorizer struct { - w io.Writer -} - -func (a labelColorizer) Write(p []byte) (n int, err error) { - replaced := errorRe.ReplaceAllStringFunc(string(p), func(m string) string { - switch m { - case "ERROR", "FATAL": - return terminal.Error(m) - case "WARN": - return terminal.Warning(m) - default: - return m - } - }) - // io.MultiWriter will abort if we return a bigger write count than input - // bytes, so we lie a little. - _, err = a.w.Write([]byte(replaced)) - return len(p), err -} - -// InitGlobalLogger initializes the global logger, used in some rare cases. -func InitGlobalLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer) { - outHandle, logHandle = getLogWriters(outHandle, logHandle) - - jww.SetStdoutOutput(outHandle) - jww.SetLogOutput(logHandle) - jww.SetLogThreshold(logThreshold) - jww.SetStdoutThreshold(stdoutThreshold) -} - -func getLogWriters(outHandle, logHandle io.Writer) (io.Writer, io.Writer) { - isTerm := terminal.PrintANSIColors(os.Stdout) - if logHandle != io.Discard && isTerm { - // Remove any Ansi coloring from log output - logHandle = ansiCleaner{w: logHandle} - } - - if isTerm { - outHandle = labelColorizer{w: outHandle} - } - - return outHandle, logHandle -} - -type fatalLogWriter int - -func (s fatalLogWriter) Write(p []byte) (n int, err error) { - trace := make([]byte, 1500) - runtime.Stack(trace, true) - fmt.Printf("\n===========\n\n%s\n", trace) - os.Exit(-1) - - return 0, nil -} - -var fatalLogListener = func(t jww.Threshold) io.Writer { - if t != jww.LevelError { - // Only interested in ERROR - return nil - } - - return new(fatalLogWriter) -} - -func newLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *logger { - errorCounter := &jww.Counter{} - warnCounter := &jww.Counter{} - outHandle, logHandle = getLogWriters(outHandle, logHandle) - - listeners := []jww.LogListener{jww.LogCounter(errorCounter, jww.LevelError), jww.LogCounter(warnCounter, jww.LevelWarn)} - var errorBuff *bytes.Buffer - if saveErrors { - errorBuff = new(bytes.Buffer) - errorCapture := func(t jww.Threshold) io.Writer { - if t != jww.LevelError { - // Only interested in ERROR - return nil - } - return errorBuff - } - - listeners = append(listeners, errorCapture) - } - - return &logger{ - Notepad: jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime, listeners...), - out: outHandle, - logCounters: &LogCounters{ - ErrorCounter: errorCounter, - WarnCounter: warnCounter, - }, - errors: errorBuff, - } -} diff --git a/common/loggers/loggers_test.go b/common/loggers/loggers_test.go deleted file mode 100644 index a7bd1ae12..000000000 --- a/common/loggers/loggers_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package loggers - -import ( - "bytes" - "fmt" - "log" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestLogger(t *testing.T) { - c := qt.New(t) - l := NewWarningLogger() - - l.Errorln("One error") - l.Errorln("Two error") - l.Warnln("A warning") - - c.Assert(l.LogCounters().ErrorCounter.Count(), qt.Equals, uint64(2)) -} - -func TestLoggerToWriterWithPrefix(t *testing.T) { - c := qt.New(t) - - var b bytes.Buffer - - logger := log.New(&b, "", 0) - - w := LoggerToWriterWithPrefix(logger, "myprefix") - - fmt.Fprint(w, "Hello Hugo!") - - c.Assert(b.String(), qt.Equals, "myprefix: Hello Hugo!\n") -} - -func TestRemoveANSIColours(t *testing.T) { - c := qt.New(t) - - c.Assert(RemoveANSIColours(""), qt.Equals, "") - c.Assert(RemoveANSIColours("\033[31m"), qt.Equals, "") - c.Assert(RemoveANSIColours("\033[31mHello"), qt.Equals, "Hello") - c.Assert(RemoveANSIColours("\033[31mHello\033[0m"), qt.Equals, "Hello") - c.Assert(RemoveANSIColours("\033[31mHello\033[0m World"), qt.Equals, "Hello World") - c.Assert(RemoveANSIColours("\033[31mHello\033[0m World\033[31m!"), qt.Equals, "Hello World!") - c.Assert(RemoveANSIColours("\x1b[90m 5 |"), qt.Equals, " 5 |") -} diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go index 08792d870..ec7895eea 100644 --- a/config/allconfig/allconfig.go +++ b/config/allconfig/allconfig.go @@ -490,6 +490,9 @@ type RootConfig struct { // ENable to print warnings for multiple files published to the same destination. LogPathWarnings bool + // Enable to panic on warning log entries. This may make it easier to detect the source. + PanicOnWarning bool + // The configured environment. Default is "development" for server and "production" for build. Environment string diff --git a/config/allconfig/load.go b/config/allconfig/load.go index eca9d06df..4e5478c40 100644 --- a/config/allconfig/load.go +++ b/config/allconfig/load.go @@ -46,7 +46,7 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) { } if d.Logger == nil { - d.Logger = loggers.NewErrorLogger() + d.Logger = loggers.NewDefault() } l := &configLoader{ConfigSourceDescriptor: d, cfg: config.New()} @@ -90,8 +90,9 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) { return nil, fmt.Errorf("failed to init config: %w", err) } - // This is unfortunate, but this is a global setting. + // This is unfortunate, but these are global settings. tpl.SetSecurityAllowActionJSTmpl(configs.Base.Security.GoTemplates.AllowActionJSTmpl) + loggers.InitGlobalLogger(configs.Base.PanicOnWarning) return configs, nil diff --git a/config/commonConfig.go b/config/commonConfig.go index bd3e235bd..ac1dd39fa 100644 --- a/config/commonConfig.go +++ b/config/commonConfig.go @@ -19,6 +19,7 @@ import ( "sort" "strings" + "github.com/bep/logg" "github.com/gobwas/glob" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/types" @@ -306,6 +307,7 @@ func (c *CacheBuster) CompileConfig(logger loggers.Logger) error { if c.compiledSource != nil { return nil } + source := c.Source target := c.Target sourceRe, err := regexp.Compile(source) @@ -313,6 +315,8 @@ func (c *CacheBuster) CompileConfig(logger loggers.Logger) error { return fmt.Errorf("failed to compile cache buster source %q: %w", c.Source, err) } var compileErr error + debugl := logger.Logger().WithLevel(logg.LevelDebug).WithField(loggers.FieldNameCmd, "cachebuster") + c.compiledSource = func(s string) func(string) bool { m := sourceRe.FindStringSubmatch(s) matchString := "no match" @@ -320,7 +324,7 @@ func (c *CacheBuster) CompileConfig(logger loggers.Logger) error { if match { matchString = "match!" } - logger.Debugf("cachebuster: Matching %q with source %q: %s\n", s, source, matchString) + debugl.Logf("Matching %q with source %q: %s", s, source, matchString) if !match { return nil } @@ -341,7 +345,7 @@ func (c *CacheBuster) CompileConfig(logger loggers.Logger) error { if match { matchString = "match!" } - logger.Debugf("cachebuster: Matching %q with target %q: %s\n", s, target, matchString) + logger.Debugf("Matching %q with target %q: %s", s, target, matchString) return match } diff --git a/config/commonConfig_test.go b/config/commonConfig_test.go index 106069bdc..b8130eb0d 100644 --- a/config/commonConfig_test.go +++ b/config/commonConfig_test.go @@ -92,7 +92,7 @@ status = 301 s, err := DecodeServer(cfg) c.Assert(err, qt.IsNil) - c.Assert(s.CompileConfig(loggers.NewErrorLogger()), qt.IsNil) + c.Assert(s.CompileConfig(loggers.NewDefault()), qt.IsNil) c.Assert(s.MatchHeaders("/foo.jpg"), qt.DeepEquals, []types.KeyValueStr{ {Key: "X-Content-Type-Options", Value: "nosniff"}, @@ -145,7 +145,7 @@ func TestBuildConfigCacheBusters(t *testing.T) { c := qt.New(t) cfg := New() conf := DecodeBuildConfig(cfg) - l := loggers.NewInfoLogger() + l := loggers.NewDefault() c.Assert(conf.CompileConfig(l), qt.IsNil) m, err := conf.MatchCacheBuster(l, "assets/foo/main.js") diff --git a/deploy/deploy.go b/deploy/deploy.go index db88996a9..60a3da363 100644 --- a/deploy/deploy.go +++ b/deploy/deploy.go @@ -37,10 +37,10 @@ import ( "github.com/dustin/go-humanize" "github.com/gobwas/glob" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/media" "github.com/spf13/afero" - jww "github.com/spf13/jwalterweatherman" "golang.org/x/text/unicode/norm" "gocloud.dev/blob" @@ -56,9 +56,10 @@ type Deployer struct { bucket *blob.Bucket mediaTypes media.Types // Hugo's MediaType to guess ContentType - quiet bool // true reduces STDOUT + quiet bool // true reduces STDOUT // TODO(bep) remove, this is a global feature. - cfg DeployConfig + cfg DeployConfig + logger loggers.Logger target *Target // the target to deploy to @@ -73,7 +74,7 @@ type deploySummary struct { const metaMD5Hash = "md5chksum" // the meta key to store md5hash in // New constructs a new *Deployer. -func New(cfg config.AllProvider, localFs afero.Fs) (*Deployer, error) { +func New(cfg config.AllProvider, logger loggers.Logger, localFs afero.Fs) (*Deployer, error) { dcfg := cfg.GetConfigSection(deploymentConfigKey).(DeployConfig) targetName := dcfg.Target @@ -112,12 +113,16 @@ func (d *Deployer) openBucket(ctx context.Context) (*blob.Bucket, error) { if d.bucket != nil { return d.bucket, nil } - jww.FEEDBACK.Printf("Deploying to target %q (%s)\n", d.target.Name, d.target.URL) + d.logger.Printf("Deploying to target %q (%s)\n", d.target.Name, d.target.URL) return blob.OpenBucket(ctx, d.target.URL) } // Deploy deploys the site to a target. func (d *Deployer) Deploy(ctx context.Context) error { + if d.logger == nil { + d.logger = loggers.NewDefault() + } + bucket, err := d.openBucket(ctx) if err != nil { return err @@ -132,33 +137,33 @@ func (d *Deployer) Deploy(ctx context.Context) error { if d.target != nil { include, exclude = d.target.includeGlob, d.target.excludeGlob } - local, err := walkLocal(d.localFs, d.cfg.Matchers, include, exclude, d.mediaTypes) + local, err := d.walkLocal(d.localFs, d.cfg.Matchers, include, exclude, d.mediaTypes) if err != nil { return err } - jww.INFO.Printf("Found %d local files.\n", len(local)) + d.logger.Infof("Found %d local files.\n", len(local)) d.summary.NumLocal = len(local) // Load remote files from the target. - remote, err := walkRemote(ctx, bucket, include, exclude) + remote, err := d.walkRemote(ctx, bucket, include, exclude) if err != nil { return err } - jww.INFO.Printf("Found %d remote files.\n", len(remote)) + d.logger.Infof("Found %d remote files.\n", len(remote)) d.summary.NumRemote = len(remote) // Diff local vs remote to see what changes need to be applied. - uploads, deletes := findDiffs(local, remote, d.cfg.Force) + uploads, deletes := d.findDiffs(local, remote, d.cfg.Force) d.summary.NumUploads = len(uploads) d.summary.NumDeletes = len(deletes) if len(uploads)+len(deletes) == 0 { if !d.quiet { - jww.FEEDBACK.Println("No changes required.") + d.logger.Println("No changes required.") } return nil } if !d.quiet { - jww.FEEDBACK.Println(summarizeChanges(uploads, deletes)) + d.logger.Println(summarizeChanges(uploads, deletes)) } // Ask for confirmation before proceeding. @@ -192,14 +197,14 @@ func (d *Deployer) Deploy(ctx context.Context) error { for _, upload := range uploads { if d.cfg.DryRun { if !d.quiet { - jww.FEEDBACK.Printf("[DRY RUN] Would upload: %v\n", upload) + d.logger.Printf("[DRY RUN] Would upload: %v\n", upload) } continue } sem <- struct{}{} go func(upload *fileToUpload) { - if err := doSingleUpload(ctx, bucket, upload); err != nil { + if err := d.doSingleUpload(ctx, bucket, upload); err != nil { errMu.Lock() defer errMu.Unlock() errs = append(errs, err) @@ -214,7 +219,7 @@ func (d *Deployer) Deploy(ctx context.Context) error { } if d.cfg.MaxDeletes != -1 && len(deletes) > d.cfg.MaxDeletes { - jww.WARN.Printf("Skipping %d deletes because it is more than --maxDeletes (%d). If this is expected, set --maxDeletes to a larger number, or -1 to disable this check.\n", len(deletes), d.cfg.MaxDeletes) + d.logger.Warnf("Skipping %d deletes because it is more than --maxDeletes (%d). If this is expected, set --maxDeletes to a larger number, or -1 to disable this check.\n", len(deletes), d.cfg.MaxDeletes) d.summary.NumDeletes = 0 } else { // Apply deletes in parallel. @@ -223,16 +228,16 @@ func (d *Deployer) Deploy(ctx context.Context) error { for _, del := range deletes { if d.cfg.DryRun { if !d.quiet { - jww.FEEDBACK.Printf("[DRY RUN] Would delete %s\n", del) + d.logger.Printf("[DRY RUN] Would delete %s\n", del) } continue } sem <- struct{}{} go func(del string) { - jww.INFO.Printf("Deleting %s...\n", del) + d.logger.Infof("Deleting %s...\n", del) if err := bucket.Delete(ctx, del); err != nil { if gcerrors.Code(err) == gcerrors.NotFound { - jww.WARN.Printf("Failed to delete %q because it wasn't found: %v", del, err) + d.logger.Warnf("Failed to delete %q because it wasn't found: %v", del, err) } else { errMu.Lock() defer errMu.Unlock() @@ -250,24 +255,24 @@ func (d *Deployer) Deploy(ctx context.Context) error { if len(errs) > 0 { if !d.quiet { - jww.FEEDBACK.Printf("Encountered %d errors.\n", len(errs)) + d.logger.Printf("Encountered %d errors.\n", len(errs)) } return errs[0] } if !d.quiet { - jww.FEEDBACK.Println("Success!") + d.logger.Println("Success!") } if d.cfg.InvalidateCDN { if d.target.CloudFrontDistributionID != "" { if d.cfg.DryRun { if !d.quiet { - jww.FEEDBACK.Printf("[DRY RUN] Would invalidate CloudFront CDN with ID %s\n", d.target.CloudFrontDistributionID) + d.logger.Printf("[DRY RUN] Would invalidate CloudFront CDN with ID %s\n", d.target.CloudFrontDistributionID) } } else { - jww.FEEDBACK.Println("Invalidating CloudFront CDN...") + d.logger.Println("Invalidating CloudFront CDN...") if err := InvalidateCloudFront(ctx, d.target.CloudFrontDistributionID); err != nil { - jww.FEEDBACK.Printf("Failed to invalidate CloudFront CDN: %v\n", err) + d.logger.Printf("Failed to invalidate CloudFront CDN: %v\n", err) return err } } @@ -275,17 +280,17 @@ func (d *Deployer) Deploy(ctx context.Context) error { if d.target.GoogleCloudCDNOrigin != "" { if d.cfg.DryRun { if !d.quiet { - jww.FEEDBACK.Printf("[DRY RUN] Would invalidate Google Cloud CDN with origin %s\n", d.target.GoogleCloudCDNOrigin) + d.logger.Printf("[DRY RUN] Would invalidate Google Cloud CDN with origin %s\n", d.target.GoogleCloudCDNOrigin) } } else { - jww.FEEDBACK.Println("Invalidating Google Cloud CDN...") + d.logger.Println("Invalidating Google Cloud CDN...") if err := InvalidateGoogleCloudCDN(ctx, d.target.GoogleCloudCDNOrigin); err != nil { - jww.FEEDBACK.Printf("Failed to invalidate Google Cloud CDN: %v\n", err) + d.logger.Printf("Failed to invalidate Google Cloud CDN: %v\n", err) return err } } } - jww.FEEDBACK.Println("Success!") + d.logger.Println("Success!") } return nil } @@ -300,8 +305,8 @@ func summarizeChanges(uploads []*fileToUpload, deletes []string) string { } // doSingleUpload executes a single file upload. -func doSingleUpload(ctx context.Context, bucket *blob.Bucket, upload *fileToUpload) error { - jww.INFO.Printf("Uploading %v...\n", upload) +func (d *Deployer) doSingleUpload(ctx context.Context, bucket *blob.Bucket, upload *fileToUpload) error { + d.logger.Infof("Uploading %v...\n", upload) opts := &blob.WriterOptions{ CacheControl: upload.Local.CacheControl(), ContentEncoding: upload.Local.ContentEncoding(), @@ -479,7 +484,7 @@ func knownHiddenDirectory(name string) bool { // walkLocal walks the source directory and returns a flat list of files, // using localFile.SlashPath as the map keys. -func walkLocal(fs afero.Fs, matchers []*Matcher, include, exclude glob.Glob, mediaTypes media.Types) (map[string]*localFile, error) { +func (d *Deployer) walkLocal(fs afero.Fs, matchers []*Matcher, include, exclude glob.Glob, mediaTypes media.Types) (map[string]*localFile, error) { retval := map[string]*localFile{} err := afero.Walk(fs, "", func(path string, info os.FileInfo, err error) error { if err != nil { @@ -509,11 +514,11 @@ func walkLocal(fs afero.Fs, matchers []*Matcher, include, exclude glob.Glob, med // Check include/exclude matchers. slashpath := filepath.ToSlash(path) if include != nil && !include.Match(slashpath) { - jww.INFO.Printf(" dropping %q due to include\n", slashpath) + d.logger.Infof(" dropping %q due to include\n", slashpath) return nil } if exclude != nil && exclude.Match(slashpath) { - jww.INFO.Printf(" dropping %q due to exclude\n", slashpath) + d.logger.Infof(" dropping %q due to exclude\n", slashpath) return nil } @@ -539,7 +544,7 @@ func walkLocal(fs afero.Fs, matchers []*Matcher, include, exclude glob.Glob, med } // walkRemote walks the target bucket and returns a flat list. -func walkRemote(ctx context.Context, bucket *blob.Bucket, include, exclude glob.Glob) (map[string]*blob.ListObject, error) { +func (d *Deployer) walkRemote(ctx context.Context, bucket *blob.Bucket, include, exclude glob.Glob) (map[string]*blob.ListObject, error) { retval := map[string]*blob.ListObject{} iter := bucket.List(nil) for { @@ -552,11 +557,11 @@ func walkRemote(ctx context.Context, bucket *blob.Bucket, include, exclude glob. } // Check include/exclude matchers. if include != nil && !include.Match(obj.Key) { - jww.INFO.Printf(" remote dropping %q due to include\n", obj.Key) + d.logger.Infof(" remote dropping %q due to include\n", obj.Key) continue } if exclude != nil && exclude.Match(obj.Key) { - jww.INFO.Printf(" remote dropping %q due to exclude\n", obj.Key) + d.logger.Infof(" remote dropping %q due to exclude\n", obj.Key) continue } // If the remote didn't give us an MD5, use remote attributes MD5, if that doesn't exist compute one. @@ -629,7 +634,7 @@ func (u *fileToUpload) String() string { // findDiffs diffs localFiles vs remoteFiles to see what changes should be // applied to the remote target. It returns a slice of *fileToUpload and a // slice of paths for files to delete. -func findDiffs(localFiles map[string]*localFile, remoteFiles map[string]*blob.ListObject, force bool) ([]*fileToUpload, []string) { +func (d *Deployer) findDiffs(localFiles map[string]*localFile, remoteFiles map[string]*blob.ListObject, force bool) ([]*fileToUpload, []string) { var uploads []*fileToUpload var deletes []string @@ -680,10 +685,10 @@ func findDiffs(localFiles map[string]*localFile, remoteFiles map[string]*blob.Li reason = reasonNotFound } if upload { - jww.DEBUG.Printf("%s needs to be uploaded: %v\n", path, reason) + d.logger.Debugf("%s needs to be uploaded: %v\n", path, reason) uploads = append(uploads, &fileToUpload{lf, reason}) } else { - jww.DEBUG.Printf("%s exists at target and does not need to be uploaded", path) + d.logger.Debugf("%s exists at target and does not need to be uploaded", path) } } diff --git a/deploy/deploy_test.go b/deploy/deploy_test.go index fe874fbbd..66eece10b 100644 --- a/deploy/deploy_test.go +++ b/deploy/deploy_test.go @@ -30,6 +30,7 @@ import ( "sort" "testing" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/media" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -197,7 +198,8 @@ func TestFindDiffs(t *testing.T) { for _, r := range tc.Remote { remote[r.Key] = r } - gotUpdates, gotDeletes := findDiffs(local, remote, tc.Force) + d := newDeployer() + gotUpdates, gotDeletes := d.findDiffs(local, remote, tc.Force) gotUpdates = applyOrdering(nil, gotUpdates)[0] sort.Slice(gotDeletes, func(i, j int) bool { return gotDeletes[i] < gotDeletes[j] }) if diff := cmp.Diff(gotUpdates, tc.WantUpdates, cmpopts.IgnoreUnexported(localFile{})); diff != "" { @@ -249,7 +251,8 @@ func TestWalkLocal(t *testing.T) { fd.Close() } } - if got, err := walkLocal(fs, nil, nil, nil, media.DefaultTypes); err != nil { + d := newDeployer() + if got, err := d.walkLocal(fs, nil, nil, nil, media.DefaultTypes); err != nil { t.Fatal(err) } else { expect := map[string]any{} @@ -1026,3 +1029,9 @@ func verifyRemote(ctx context.Context, bucket *blob.Bucket, local []*fileData) ( } return diff, nil } + +func newDeployer() *Deployer { + return &Deployer{ + logger: loggers.NewDefault(), + } +} diff --git a/deps/deps.go b/deps/deps.go index 39462de96..309555080 100644 --- a/deps/deps.go +++ b/deps/deps.go @@ -3,12 +3,14 @@ package deps import ( "context" "fmt" + "io" "path/filepath" "sort" "strings" "sync" "sync/atomic" + "github.com/bep/logg" "github.com/gohugoio/hugo/common/hexec" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/config" @@ -25,7 +27,6 @@ import ( "github.com/gohugoio/hugo/source" "github.com/gohugoio/hugo/tpl" "github.com/spf13/afero" - jww "github.com/spf13/jwalterweatherman" ) // Deps holds dependencies used by many. @@ -36,9 +37,6 @@ type Deps struct { // The logger to use. Log loggers.Logger `json:"-"` - // Used to log errors that may repeat itself many times. - LogDistinct loggers.Logger - ExecHelper *hexec.Exec // The templates to use. This will usually implement the full tpl.TemplateManager. @@ -117,15 +115,13 @@ func (d *Deps) Init() error { } if d.Log == nil { - d.Log = loggers.NewErrorLogger() - } - - if d.LogDistinct == nil { - d.LogDistinct = helpers.NewDistinctLogger(d.Log) + d.Log = loggers.NewDefault() } if d.globalErrHandler == nil { - d.globalErrHandler = &globalErrHandler{} + d.globalErrHandler = &globalErrHandler{ + logger: d.Log, + } } if d.BuildState == nil { @@ -228,6 +224,8 @@ func (d *Deps) Compile(prototype *Deps) error { } type globalErrHandler struct { + logger loggers.Logger + // Channel for some "hard to get to" build errors buildErrors chan error // Used to signal that the build is done. @@ -246,8 +244,7 @@ func (e *globalErrHandler) SendError(err error) { } return } - - jww.ERROR.Println(err) + e.logger.Errorln(err) } func (e *globalErrHandler) StartErrorCollector() chan error { @@ -312,9 +309,16 @@ func (d *Deps) Close() error { // on a global level, i.e. logging etc. // Nil values will be given default values. type DepsCfg struct { + // The logger to use. Only set in some tests. + // TODO(bep) get rid of this. + TestLogger loggers.Logger - // The Logger to use. - Logger loggers.Logger + // The logging level to use. + LogLevel logg.Level + + // Where to write the logs. + // Currently we typically write everything to stdout. + LogOut io.Writer // The file systems to use Fs *hugofs.Fs diff --git a/go.mod b/go.mod index 6371423e0..92939a086 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/bep/gowebp v0.2.0 github.com/bep/helpers v0.4.0 github.com/bep/lazycache v0.2.0 + github.com/bep/logg v0.2.0 github.com/bep/mclib v1.20400.20402 github.com/bep/overlayfs v0.6.0 github.com/bep/simplecobra v0.3.2 @@ -25,6 +26,7 @@ require ( github.com/disintegration/gift v1.2.1 github.com/dustin/go-humanize v1.0.1 github.com/evanw/esbuild v0.18.3 + github.com/fatih/color v1.15.0 github.com/fortytw2/leaktest v1.3.0 github.com/frankban/quicktest v1.14.5 github.com/fsnotify/fsnotify v1.6.0 @@ -58,7 +60,6 @@ require ( github.com/spf13/cast v1.5.1 github.com/spf13/cobra v1.7.0 github.com/spf13/fsync v0.9.0 - github.com/spf13/jwalterweatherman v1.1.0 github.com/spf13/pflag v1.0.5 github.com/tdewolff/minify/v2 v2.12.6 github.com/tdewolff/parse/v2 v2.6.6 @@ -109,6 +110,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.18.3 // indirect github.com/aws/smithy-go v1.13.5 // indirect + github.com/bep/clocks v0.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect @@ -129,11 +131,13 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/perimeterx/marshmallow v1.1.4 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/crypto v0.10.0 // indirect diff --git a/go.sum b/go.sum index 7ba2c773b..ffadc9c48 100644 --- a/go.sum +++ b/go.sum @@ -612,6 +612,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bep/clock v0.3.0 h1:vfOA6+wVb6pPQEiXow9f/too92vNTLe9MuwO13PfI0M= github.com/bep/clock v0.3.0/go.mod h1:6Gz2lapnJ9vxpvPxQ2u6FcXFRoj4kkiqQ6pm0ERZlwk= +github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= +github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU= github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo= github.com/bep/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bep/gitmap v1.1.2 h1:zk04w1qc1COTZPPYWDQHvns3y1afOsdRfraFQ3qI840= @@ -630,6 +632,8 @@ github.com/bep/helpers v0.4.0 h1:ab9veaAiWY4ST48Oxp5usaqivDmYdB744fz+tcZ3Ifs= github.com/bep/helpers v0.4.0/go.mod h1:/QpHdmcPagDw7+RjkLFCvnlUc8lQ5kg4KDrEkb2Yyco= github.com/bep/lazycache v0.2.0 h1:HKrlZTrDxHIrNKqmnurH42ryxkngCMYLfBpyu40VcwY= github.com/bep/lazycache v0.2.0/go.mod h1:xUIsoRD824Vx0Q/n57+ZO7kmbEhMBOnTjM/iPixNGbg= +github.com/bep/logg v0.2.0 h1:EWKB04ea/K/V0xd/7O6x5q+1l+Grub+9N48YMcevtF4= +github.com/bep/logg v0.2.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0= github.com/bep/mclib v1.20400.20402 h1:olpCE2WSPpOAbFE1R4hnftSEmQ34+xzy2HRzd0m69rA= github.com/bep/mclib v1.20400.20402/go.mod h1:pkrk9Kyfqg34Uj6XlDq9tdEFJBiL1FvCoCgVKRzw1EY= github.com/bep/overlayfs v0.6.0 h1:sgLcq/qtIzbaQNl2TldGXOkHvqeZB025sPvHOQL+DYo= @@ -916,6 +920,8 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= @@ -1496,6 +1502,8 @@ github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -1505,6 +1513,7 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= @@ -1692,6 +1701,7 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= @@ -1840,7 +1850,6 @@ github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRM github.com/spf13/fsync v0.9.0 h1:f9CEt3DOB2mnHxZaftmEOFWjABEvKM/xpf3cUwJrGOY= github.com/spf13/fsync v0.9.0/go.mod h1:fNtJEfG3HiltN3y4cPOz6MLjos9+2pIEqLIgszqhp/0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -2441,6 +2450,7 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/helpers/general.go b/helpers/general.go index 50f7920f6..d1df2e335 100644 --- a/helpers/general.go +++ b/helpers/general.go @@ -24,13 +24,11 @@ import ( "path/filepath" "sort" "strings" - "sync" "unicode" "unicode/utf8" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/common/hugo" + "github.com/gohugoio/hugo/common/loggers" "github.com/spf13/afero" @@ -254,143 +252,6 @@ func compareStringSlices(a, b []string) bool { return true } -// DistinctLogger ignores duplicate log statements. -type DistinctLogger struct { - loggers.Logger - sync.RWMutex - m map[string]bool -} - -func (l *DistinctLogger) Reset() { - l.Lock() - defer l.Unlock() - - l.m = make(map[string]bool) -} - -// Println will log the string returned from fmt.Sprintln given the arguments, -// but not if it has been logged before. -func (l *DistinctLogger) Println(v ...any) { - // fmt.Sprint doesn't add space between string arguments - logStatement := strings.TrimSpace(fmt.Sprintln(v...)) - l.printIfNotPrinted("println", logStatement, func() { - l.Logger.Println(logStatement) - }) -} - -// Printf will log the string returned from fmt.Sprintf given the arguments, -// but not if it has been logged before. -func (l *DistinctLogger) Printf(format string, v ...any) { - logStatement := fmt.Sprintf(format, v...) - l.printIfNotPrinted("printf", logStatement, func() { - l.Logger.Printf(format, v...) - }) -} - -func (l *DistinctLogger) Debugf(format string, v ...any) { - logStatement := fmt.Sprintf(format, v...) - l.printIfNotPrinted("debugf", logStatement, func() { - l.Logger.Debugf(format, v...) - }) -} - -func (l *DistinctLogger) Debugln(v ...any) { - logStatement := fmt.Sprint(v...) - l.printIfNotPrinted("debugln", logStatement, func() { - l.Logger.Debugln(v...) - }) -} - -func (l *DistinctLogger) Infof(format string, v ...any) { - logStatement := fmt.Sprintf(format, v...) - l.printIfNotPrinted("info", logStatement, func() { - l.Logger.Infof(format, v...) - }) -} - -func (l *DistinctLogger) Infoln(v ...any) { - logStatement := fmt.Sprint(v...) - l.printIfNotPrinted("infoln", logStatement, func() { - l.Logger.Infoln(v...) - }) -} - -func (l *DistinctLogger) Warnf(format string, v ...any) { - logStatement := fmt.Sprintf(format, v...) - l.printIfNotPrinted("warnf", logStatement, func() { - l.Logger.Warnf(format, v...) - }) -} - -func (l *DistinctLogger) Warnln(v ...any) { - logStatement := fmt.Sprint(v...) - l.printIfNotPrinted("warnln", logStatement, func() { - l.Logger.Warnln(v...) - }) -} - -func (l *DistinctLogger) Errorf(format string, v ...any) { - logStatement := fmt.Sprint(v...) - l.printIfNotPrinted("errorf", logStatement, func() { - l.Logger.Errorf(format, v...) - }) -} - -func (l *DistinctLogger) Errorln(v ...any) { - logStatement := fmt.Sprint(v...) - l.printIfNotPrinted("errorln", logStatement, func() { - l.Logger.Errorln(v...) - }) -} - -func (l *DistinctLogger) hasPrinted(key string) bool { - l.RLock() - defer l.RUnlock() - _, found := l.m[key] - return found -} - -func (l *DistinctLogger) printIfNotPrinted(level, logStatement string, print func()) { - key := level + logStatement - if l.hasPrinted(key) { - return - } - l.Lock() - defer l.Unlock() - l.m[key] = true // Placing this after print() can cause duplicate warning entries to be logged when --panicOnWarning is true. - print() - -} - -// NewDistinctErrorLogger creates a new DistinctLogger that logs ERRORs -func NewDistinctErrorLogger() loggers.Logger { - return &DistinctLogger{m: make(map[string]bool), Logger: loggers.NewErrorLogger()} -} - -// NewDistinctLogger creates a new DistinctLogger that logs to the provided logger. -func NewDistinctLogger(logger loggers.Logger) loggers.Logger { - return &DistinctLogger{m: make(map[string]bool), Logger: logger} -} - -// NewDistinctWarnLogger creates a new DistinctLogger that logs WARNs -func NewDistinctWarnLogger() loggers.Logger { - return &DistinctLogger{m: make(map[string]bool), Logger: loggers.NewWarningLogger()} -} - -var ( - // DistinctErrorLog can be used to avoid spamming the logs with errors. - DistinctErrorLog = NewDistinctErrorLogger() - - // DistinctWarnLog can be used to avoid spamming the logs with warnings. - DistinctWarnLog = NewDistinctWarnLogger() -) - -// InitLoggers resets the global distinct loggers. -func InitLoggers() { - DistinctErrorLog.Reset() - DistinctWarnLog.Reset() -} - // Deprecated informs about a deprecation, but only once for a given set of arguments' values. // If the err flag is enabled, it logs as an ERROR (will exit with -1) and the text will // point at the next Hugo release. @@ -398,13 +259,9 @@ func InitLoggers() { // plenty of time to fix their templates. func Deprecated(item, alternative string, err bool) { if err { - DistinctErrorLog.Errorf("%s is deprecated and will be removed in Hugo %s. %s", item, hugo.CurrentVersion.Next().ReleaseVersion(), alternative) + loggers.Log().Errorf("%s is deprecated and will be removed in Hugo %s. %s", item, hugo.CurrentVersion.Next().ReleaseVersion(), alternative) } else { - var warnPanicMessage string - if !loggers.PanicOnWarning.Load() { - warnPanicMessage = "\n\nRe-run Hugo with the flag --panicOnWarning to get a better error message." - } - DistinctWarnLog.Warnf("%s is deprecated and will be removed in a future release. %s%s", item, alternative, warnPanicMessage) + loggers.Log().Warnf("%s is deprecated and will be removed in a future release. %s%s", item, alternative) } } diff --git a/helpers/general_test.go b/helpers/general_test.go index 9b2e4fc58..827411027 100644 --- a/helpers/general_test.go +++ b/helpers/general_test.go @@ -18,9 +18,7 @@ import ( "reflect" "strings" "testing" - "time" - "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/helpers" qt "github.com/frankban/quicktest" @@ -55,60 +53,6 @@ func TestResolveMarkup(t *testing.T) { } } -func TestDistinctLoggerDoesNotLockOnWarningPanic(t *testing.T) { - // Testing to make sure logger mutex doesn't lock if warnings cause panics. - // func Warnf() of DistinctLogger is defined in general.go - l := helpers.NewDistinctLogger(loggers.NewWarningLogger()) - - // Set PanicOnWarning to true to reproduce issue 9380 - // Ensure global variable loggers.PanicOnWarning is reset to old value after test - if !loggers.PanicOnWarning.Load() { - loggers.PanicOnWarning.Store(true) - defer func() { - loggers.PanicOnWarning.Store(false) - }() - } - - // Establish timeout in case a lock occurs: - timeIsUp := make(chan bool) - timeOutSeconds := 1 - go func() { - time.Sleep(time.Second * time.Duration(timeOutSeconds)) - timeIsUp <- true - }() - - // Attempt to run multiple logging threads in parallel - counterC := make(chan int) - goroutines := 5 - - for i := 0; i < goroutines; i++ { - go func() { - defer func() { - // Intentional panic successfully recovered - notify counter channel - recover() - counterC <- 1 - }() - - l.Warnf("Placeholder template message: %v", "In this test, logging a warning causes a panic.") - }() - } - - // All goroutines should complete before timeout - var counter int - for { - select { - case <-counterC: - counter++ - if counter == goroutines { - return - } - case <-timeIsUp: - t.Errorf("Unable to log warnings with --panicOnWarning within alloted time of: %v seconds. Investigate possible mutex locking on panic in distinct warning logger.", timeOutSeconds) - return - } - } -} - func TestFirstUpper(t *testing.T) { for i, this := range []struct { in string diff --git a/helpers/testhelpers_test.go b/helpers/testhelpers_test.go index be8983fdb..6c5f62488 100644 --- a/helpers/testhelpers_test.go +++ b/helpers/testhelpers_test.go @@ -23,7 +23,7 @@ func newTestPathSpecFromCfgAndLang(cfg config.Provider, lang string) *helpers.Pa } } fs := hugofs.NewFrom(mfs, conf.BaseConfig()) - ps, err := helpers.NewPathSpec(fs, conf, loggers.NewErrorLogger()) + ps, err := helpers.NewPathSpec(fs, conf, loggers.NewDefault()) if err != nil { panic(err) } @@ -41,7 +41,7 @@ func newTestPathSpec(configKeyValues ...any) *helpers.PathSpec { func newTestContentSpec(cfg config.Provider) *helpers.ContentSpec { fs := afero.NewMemMapFs() conf := testconfig.GetTestConfig(fs, cfg) - spec, err := helpers.NewContentSpec(conf, loggers.NewErrorLogger(), fs, nil) + spec, err := helpers.NewContentSpec(conf, loggers.NewDefault(), fs, nil) if err != nil { panic(err) } diff --git a/hugofs/nosymlink_fs.go b/hugofs/nosymlink_fs.go index d3cad5e74..af559844f 100644 --- a/hugofs/nosymlink_fs.go +++ b/hugofs/nosymlink_fs.go @@ -19,7 +19,6 @@ import ( "path/filepath" "github.com/gohugoio/hugo/common/loggers" - "github.com/spf13/afero" ) diff --git a/hugofs/nosymlink_test.go b/hugofs/nosymlink_test.go index e00dcf1a8..d0a8baaaa 100644 --- a/hugofs/nosymlink_test.go +++ b/hugofs/nosymlink_test.go @@ -18,8 +18,8 @@ import ( "path/filepath" "testing" + "github.com/bep/logg" "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/htesting" "github.com/spf13/afero" @@ -64,11 +64,11 @@ func TestNoSymlinkFs(t *testing.T) { blogDir := filepath.Join(workDir, "blog") blogFile1 := filepath.Join(blogDir, "a.txt") - logger := loggers.NewWarningLogger() + logger := loggers.NewDefault() for _, bfs := range []afero.Fs{NewBaseFileDecorator(Os), Os} { for _, allowFiles := range []bool{false, true} { - logger.LogCounters().WarnCounter.Reset() + logger.Reset() fs := NewNoSymlinkFs(bfs, logger, allowFiles) ls := fs.(afero.Lstater) symlinkedDir := filepath.Join(workDir, "symlinkdedir") @@ -139,7 +139,7 @@ func TestNoSymlinkFs(t *testing.T) { _, err = f.Readdir(-1) c.Assert(err, qt.IsNil) f.Close() - c.Assert(logger.LogCounters().WarnCounter.Count(), qt.Equals, uint64(1)) + c.Assert(logger.LoggCount(logg.LevelWarn), qt.Equals, 1) } } diff --git a/hugofs/walk.go b/hugofs/walk.go index e847174c6..e883f892e 100644 --- a/hugofs/walk.go +++ b/hugofs/walk.go @@ -86,7 +86,7 @@ func NewWalkway(cfg WalkwayConfig) *Walkway { logger := cfg.Logger if logger == nil { - logger = loggers.NewWarningLogger() + logger = loggers.NewDefault() } return &Walkway{ diff --git a/hugolib/alias.go b/hugolib/alias.go index d10f140bd..5165edb04 100644 --- a/hugolib/alias.go +++ b/hugolib/alias.go @@ -25,7 +25,6 @@ import ( "strings" "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/publisher" "github.com/gohugoio/hugo/resources/page" diff --git a/hugolib/alias_test.go b/hugolib/alias_test.go index 124c9f4ca..e03107ada 100644 --- a/hugolib/alias_test.go +++ b/hugolib/alias_test.go @@ -18,9 +18,8 @@ import ( "runtime" "testing" - "github.com/gohugoio/hugo/common/loggers" - qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/common/loggers" ) const pageWithAlias = `--- @@ -117,7 +116,7 @@ func TestAliasTemplate(t *testing.T) { } func TestTargetPathHTMLRedirectAlias(t *testing.T) { - h := newAliasHandler(nil, loggers.NewErrorLogger(), false) + h := newAliasHandler(nil, loggers.NewDefault(), false) errIsNilForThisOS := runtime.GOOS != "windows" diff --git a/hugolib/config_test.go b/hugolib/config_test.go index 5a3087207..02d25fa9b 100644 --- a/hugolib/config_test.go +++ b/hugolib/config_test.go @@ -20,6 +20,7 @@ import ( "strings" "testing" + "github.com/bep/logg" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config/allconfig" @@ -933,7 +934,7 @@ LanguageCode: {{ eq site.LanguageCode site.Language.LanguageCode }}|{{ site.Lang ).Build() { - b.Assert(b.H.Log.LogCounters().WarnCounter.Count(), qt.Equals, uint64(2)) + b.Assert(b.H.Log.LoggCount(logg.LevelWarn), qt.Equals, 1) } b.AssertFileContent("public/index.html", ` AllPages: 4| diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go index ed0d36de1..d5c94799d 100644 --- a/hugolib/filesystems/basefs.go +++ b/hugolib/filesystems/basefs.go @@ -29,9 +29,9 @@ import ( "github.com/gohugoio/hugo/hugofs/glob" "github.com/gohugoio/hugo/common/herrors" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/types" - "github.com/gohugoio/hugo/common/loggers" "github.com/rogpeppe/go-internal/lockedfile" "github.com/gohugoio/hugo/hugofs/files" @@ -471,7 +471,7 @@ var counter int func NewBase(p *paths.Paths, logger loggers.Logger, options ...func(*BaseFs) error) (*BaseFs, error) { fs := p.Fs if logger == nil { - logger = loggers.NewWarningLogger() + logger = loggers.NewDefault() } publishFs := hugofs.NewBaseFileDecorator(fs.PublishDir) diff --git a/hugolib/hugo_modules_test.go b/hugolib/hugo_modules_test.go index ee4ef798a..3353f508e 100644 --- a/hugolib/hugo_modules_test.go +++ b/hugolib/hugo_modules_test.go @@ -22,16 +22,16 @@ import ( "testing" "time" + "github.com/bep/logg" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/modules/npm" - "github.com/gohugoio/hugo/common/loggers" - "github.com/spf13/afero" "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/common/hugo" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/hugofs" @@ -646,14 +646,14 @@ min_version = 0.55.0 `) - logger := loggers.NewWarningLogger() + logger := loggers.NewDefault() b.WithLogger(logger) b.Build(BuildCfg{}) c := qt.New(t) - c.Assert(logger.LogCounters().WarnCounter.Count(), qt.Equals, uint64(3)) + c.Assert(logger.LoggCount(logg.LevelWarn), qt.Equals, 3) } func TestModulesSymlinks(t *testing.T) { @@ -727,7 +727,7 @@ weight = 2 ` b := newTestSitesBuilder(t).WithNothingAdded().WithWorkingDir(workingDir) - b.WithLogger(loggers.NewErrorLogger()) + b.WithLogger(loggers.NewDefault()) b.Fs = fs b.WithConfigFile("toml", config) diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index 290eebe82..6ffef6fa8 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -23,6 +23,7 @@ import ( "sync" "sync/atomic" + "github.com/bep/logg" "github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/hugofs/glob" @@ -42,7 +43,6 @@ import ( "github.com/gohugoio/hugo/source" "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/lazy" @@ -265,7 +265,7 @@ func (h *HugoSites) NumLogErrors() int { if h == nil { return 0 } - return int(h.Log.LogCounters().ErrorCounter.Count()) + return h.Log.LoggCount(logg.LevelError) } func (h *HugoSites) PrintProcessingStats(w io.Writer) { @@ -352,10 +352,8 @@ func (h *HugoSites) reset(config *BuildCfg) { // resetLogs resets the log counters etc. Used to do a new build on the same sites. func (h *HugoSites) resetLogs() { h.Log.Reset() - loggers.GlobalErrorCounter.Reset() for _, s := range h.Sites { s.Deps.Log.Reset() - s.Deps.LogDistinct.Reset() } } diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index c801ae3df..d90d9ce7b 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -22,6 +22,7 @@ import ( "strings" "time" + "github.com/bep/logg" "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/publisher" @@ -65,6 +66,8 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error { defer unlock() } + infol := h.Log.InfoCommand("build") + errCollector := h.StartErrorCollector() errs := make(chan error) @@ -120,11 +123,11 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error { return nil } - if err := h.process(conf, init, events...); err != nil { + if err := h.process(infol, conf, init, events...); err != nil { return fmt.Errorf("process: %w", err) } - if err := h.assemble(conf); err != nil { + if err := h.assemble(infol, conf); err != nil { return fmt.Errorf("assemble: %w", err) } @@ -137,10 +140,10 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error { } if prepareErr == nil { - if err := h.render(conf); err != nil { + if err := h.render(infol, conf); err != nil { h.SendError(fmt.Errorf("render: %w", err)) } - if err := h.postProcess(); err != nil { + if err := h.postProcess(infol); err != nil { h.SendError(fmt.Errorf("postProcess: %w", err)) } } @@ -164,7 +167,7 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error { return err } - errorCount := h.Log.LogCounters().ErrorCounter.Count() + errorCount := h.Log.LoggCount(logg.LevelError) if errorCount > 0 { return fmt.Errorf("logged %d error(s)", errorCount) } @@ -195,13 +198,12 @@ func (h *HugoSites) initRebuild(config *BuildCfg) error { h.reset(config) h.resetLogs() - helpers.InitLoggers() return nil } -func (h *HugoSites) process(config *BuildCfg, init func(config *BuildCfg) error, events ...fsnotify.Event) error { - defer h.timeTrack(time.Now(), "process") +func (h *HugoSites) process(l logg.LevelLogger, config *BuildCfg, init func(config *BuildCfg) error, events ...fsnotify.Event) error { + defer h.timeTrack(l, time.Now(), "process") // We should probably refactor the Site and pull up most of the logic from there to here, // but that seems like a daunting task. @@ -218,8 +220,8 @@ func (h *HugoSites) process(config *BuildCfg, init func(config *BuildCfg) error, return firstSite.process(*config) } -func (h *HugoSites) assemble(bcfg *BuildCfg) error { - defer h.timeTrack(time.Now(), "assemble") +func (h *HugoSites) assemble(l logg.LevelLogger, bcfg *BuildCfg) error { + defer h.timeTrack(l, time.Now(), "assemble") if !bcfg.whatChanged.source { return nil @@ -236,13 +238,13 @@ func (h *HugoSites) assemble(bcfg *BuildCfg) error { return nil } -func (h *HugoSites) timeTrack(start time.Time, name string) { +func (h *HugoSites) timeTrack(l logg.LevelLogger, start time.Time, name string) { elapsed := time.Since(start) - h.Log.Infof("%s in %v ms\n", name, int(1000*elapsed.Seconds())) + l.WithField("step", name).WithField("duration", elapsed).Logf("running") } -func (h *HugoSites) render(config *BuildCfg) error { - defer h.timeTrack(time.Now(), "render") +func (h *HugoSites) render(l logg.LevelLogger, config *BuildCfg) error { + defer h.timeTrack(l, time.Now(), "render") if _, err := h.init.layouts.Do(context.Background()); err != nil { return err } @@ -312,8 +314,8 @@ func (h *HugoSites) render(config *BuildCfg) error { return nil } -func (h *HugoSites) postProcess() error { - defer h.timeTrack(time.Now(), "postProcess") +func (h *HugoSites) postProcess(l logg.LevelLogger) error { + defer h.timeTrack(l, time.Now(), "postProcess") // Make sure to write any build stats to disk first so it's available // to the post processors. diff --git a/hugolib/hugo_sites_build_test.go b/hugolib/hugo_sites_build_test.go index b2798c863..3c82a60b6 100644 --- a/hugolib/hugo_sites_build_test.go +++ b/hugolib/hugo_sites_build_test.go @@ -1394,7 +1394,7 @@ other = %q } func TestRebuildOnAssetChange(t *testing.T) { - b := newTestSitesBuilder(t).Running().WithLogger(loggers.NewInfoLogger()) + b := newTestSitesBuilder(t).Running().WithLogger(loggers.NewDefault()) b.WithTemplatesAdded("index.html", ` {{ (resources.Get "data.json").Content }} `) diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go index 0daa766ac..cb34cb28b 100644 --- a/hugolib/integrationtest_builder.go +++ b/hugolib/integrationtest_builder.go @@ -13,7 +13,7 @@ import ( "sync" "testing" - jww "github.com/spf13/jwalterweatherman" + "github.com/bep/logg" qt "github.com/frankban/quicktest" "github.com/fsnotify/fsnotify" @@ -292,11 +292,9 @@ func (s *IntegrationTestBuilder) initBuilder() error { } if s.Cfg.LogLevel == 0 { - s.Cfg.LogLevel = jww.LevelWarn + s.Cfg.LogLevel = logg.LevelWarn } - logger := loggers.NewBasicLoggerForWriter(s.Cfg.LogLevel, &s.logBuff) - isBinaryRe := regexp.MustCompile(`^(.*)(\.png|\.jpg)$`) const dataSourceFilenamePrefix = "sourcefilename:" @@ -350,7 +348,7 @@ func (s *IntegrationTestBuilder) initBuilder() error { Flags: flags, ConfigDir: configDir, Fs: afs, - Logger: logger, + Logger: loggers.NewDefault(), Environ: s.Cfg.Environ, }, ) @@ -364,7 +362,7 @@ func (s *IntegrationTestBuilder) initBuilder() error { s.Assert(err, qt.IsNil) - depsCfg := deps.DepsCfg{Configs: res, Fs: fs, Logger: logger} + depsCfg := deps.DepsCfg{Configs: res, Fs: fs, LogLevel: s.Cfg.LogLevel, LogOut: &s.logBuff} sites, err := NewHugoSites(depsCfg) if err != nil { initErr = err @@ -528,7 +526,7 @@ type IntegrationTestConfig struct { // Will print the log buffer after the build Verbose bool - LogLevel jww.Threshold + LogLevel logg.Level // Whether it needs the real file system (e.g. for js.Build tests). NeedsOsFS bool diff --git a/hugolib/mount_filters_test.go b/hugolib/mount_filters_test.go index 688cf2558..4f6a448d2 100644 --- a/hugolib/mount_filters_test.go +++ b/hugolib/mount_filters_test.go @@ -20,7 +20,6 @@ import ( "testing" "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/htesting" @@ -39,7 +38,7 @@ func TestMountFilters(t *testing.T) { for _, component := range files.ComponentFolders { b.Assert(os.MkdirAll(filepath.Join(workingDir, component), 0777), qt.IsNil) } - b.WithWorkingDir(workingDir).WithLogger(loggers.NewInfoLogger()) + b.WithWorkingDir(workingDir).WithLogger(loggers.NewDefault()) b.WithConfigFile("toml", fmt.Sprintf(` workingDir = %q diff --git a/hugolib/page__new.go b/hugolib/page__new.go index e9a8b1a50..14db28c3d 100644 --- a/hugolib/page__new.go +++ b/hugolib/page__new.go @@ -99,7 +99,7 @@ func newPageFromMeta( meta map[string]any, metaProvider *pageMeta) (*pageState, error) { if metaProvider.f == nil { - metaProvider.f = page.NewZeroFile(metaProvider.s.LogDistinct) + metaProvider.f = page.NewZeroFile(metaProvider.s.Log) } ps, err := newPageBase(metaProvider) diff --git a/hugolib/page_test.go b/hugolib/page_test.go index 79b6401dc..6b817551e 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -17,7 +17,6 @@ import ( "context" "fmt" "html/template" - "os" "path/filepath" "strings" "testing" @@ -36,7 +35,6 @@ import ( "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/resource" - "github.com/spf13/jwalterweatherman" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/deps" @@ -739,7 +737,7 @@ Here is the last report for commits in the year 2016. It covers hrev50718-hrev50 func TestRenderStringForRegularPageTranslations(t *testing.T) { c := qt.New(t) b := newTestSitesBuilder(t) - b.WithLogger(loggers.NewBasicLoggerForWriter(jwalterweatherman.LevelError, os.Stderr)) + b.WithLogger(loggers.NewDefault()) b.WithConfigFile("toml", `baseurl = "https://example.org/" @@ -800,7 +798,7 @@ home = ["HTML", "JSON"]`) // Issue 8919 func TestContentProviderWithCustomOutputFormat(t *testing.T) { b := newTestSitesBuilder(t) - b.WithLogger(loggers.NewBasicLoggerForWriter(jwalterweatherman.LevelDebug, os.Stderr)) + b.WithLogger(loggers.NewDefault()) b.WithConfigFile("toml", `baseURL = 'http://example.org/' title = 'My New Hugo Site' @@ -1437,7 +1435,7 @@ Content:{{ .Content }} // https://github.com/gohugoio/hugo/issues/5781 func TestPageWithZeroFile(t *testing.T) { - newTestSitesBuilder(t).WithLogger(loggers.NewWarningLogger()).WithSimpleConfigFile(). + newTestSitesBuilder(t).WithLogger(loggers.NewDefault()).WithSimpleConfigFile(). WithTemplatesAdded("index.html", "{{ .File.Filename }}{{ with .File }}{{ .Dir }}{{ end }}").Build(BuildCfg{}) } diff --git a/hugolib/pagebundler_test.go b/hugolib/pagebundler_test.go index 2ec3718f0..51ce82526 100644 --- a/hugolib/pagebundler_test.go +++ b/hugolib/pagebundler_test.go @@ -20,6 +20,9 @@ import ( "path" "path/filepath" "regexp" + + "github.com/gohugoio/hugo/common/loggers" + "strings" "testing" @@ -31,7 +34,6 @@ import ( "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/htesting" @@ -93,7 +95,7 @@ func TestPageBundlerSiteRegular(t *testing.T) { c.Assert(err, qt.IsNil) - b := newTestSitesBuilderFromDepsCfg(c, deps.DepsCfg{Logger: loggers.NewErrorLogger(), Fs: fs, Configs: configs}).WithNothingAdded() + b := newTestSitesBuilderFromDepsCfg(c, deps.DepsCfg{Fs: fs, Configs: configs}).WithNothingAdded() b.Build(BuildCfg{}) @@ -1044,7 +1046,7 @@ title: %q } b := newTestSitesBuilder(t).WithConfigFile("toml", config) - b.WithLogger(loggers.NewWarningLogger()) + b.WithLogger(loggers.NewDefault()) b.WithTemplates("_default/list.html", `{{ range .Site.Pages }} {{ .Kind }}|{{ .Path }}|{{ with .CurrentSection }}CurrentSection: {{ .Path }}{{ end }}|{{ .RelPermalink }}{{ end }} @@ -1215,7 +1217,7 @@ title: %q } b := newTestSitesBuilder(t).WithConfigFile("toml", config) - b.WithLogger(loggers.NewWarningLogger()) + b.WithLogger(loggers.NewDefault()) b.WithTemplates("_default/single.html", `{{ range .Resources }} {{ .ResourceType }}|{{ .Title }}| diff --git a/hugolib/pages_capture.go b/hugolib/pages_capture.go index 8a2b875ea..c57c707de 100644 --- a/hugolib/pages_capture.go +++ b/hugolib/pages_capture.go @@ -21,6 +21,7 @@ import ( "reflect" "github.com/gohugoio/hugo/common/herrors" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/parser/pageparser" @@ -29,7 +30,6 @@ import ( "github.com/gohugoio/hugo/source" - "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/hugofs" "github.com/spf13/afero" ) diff --git a/hugolib/pages_capture_test.go b/hugolib/pages_capture_test.go index 8c1023a15..c771d30ee 100644 --- a/hugolib/pages_capture_test.go +++ b/hugolib/pages_capture_test.go @@ -53,7 +53,7 @@ func TestPagesCapture(t *testing.T) { t.Run("Collect", func(t *testing.T) { c := qt.New(t) proc := &testPagesCollectorProcessor{} - coll := newPagesCollector(sourceSpec, nil, loggers.NewErrorLogger(), nil, proc) + coll := newPagesCollector(sourceSpec, nil, loggers.NewDefault(), nil, proc) c.Assert(coll.Collect(), qt.IsNil) // 2 bundles, 3 pages. c.Assert(len(proc.items), qt.Equals, 5) diff --git a/hugolib/renderstring_test.go b/hugolib/renderstring_test.go index af66156e6..e0a4cd036 100644 --- a/hugolib/renderstring_test.go +++ b/hugolib/renderstring_test.go @@ -16,6 +16,7 @@ package hugolib import ( "testing" + "github.com/bep/logg" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/common/loggers" ) @@ -80,13 +81,13 @@ func TestRenderStringOnListPage(t *testing.T) { // Issue 9433 func TestRenderStringOnPageNotBackedByAFile(t *testing.T) { t.Parallel() - logger := loggers.NewWarningLogger() + logger := loggers.NewDefault() b := newTestSitesBuilder(t).WithLogger(logger).WithConfigFile("toml", ` disableKinds = ["page", "section", "taxonomy", "term"] `) b.WithTemplates("index.html", `{{ .RenderString "**Hello**" }}`).WithContent("p1.md", "") b.BuildE(BuildCfg{}) - b.Assert(int(logger.LogCounters().WarnCounter.Count()), qt.Equals, 0) + b.Assert(logger.LoggCount(logg.LevelWarn), qt.Equals, 0) } func TestRenderStringWithShortcode(t *testing.T) { diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go index 823f7db45..da90634aa 100644 --- a/hugolib/resource_chain_test.go +++ b/hugolib/resource_chain_test.go @@ -588,7 +588,7 @@ XML: {{ $xml.body }} } t.Parallel() - b := newTestSitesBuilder(t).WithLogger(loggers.NewErrorLogger()) + b := newTestSitesBuilder(t).WithLogger(loggers.NewDefault()) b.WithContent("_index.md", ` --- title: Home diff --git a/hugolib/site.go b/hugolib/site.go index 035f543ee..fca8bca75 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -17,7 +17,6 @@ import ( "context" "fmt" "io" - "log" "mime" "net/url" "path" @@ -27,6 +26,7 @@ import ( "strings" "time" + "github.com/bep/logg" "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/htime" "github.com/gohugoio/hugo/common/hugio" @@ -285,7 +285,7 @@ func (s *Site) isEnabled(kind string) bool { type siteRefLinker struct { s *Site - errorLogger *log.Logger + errorLogger logg.LevelLogger notFoundURL string } @@ -302,11 +302,11 @@ func newSiteRefLinker(s *Site) (siteRefLinker, error) { func (s siteRefLinker) logNotFound(ref, what string, p page.Page, position text.Position) { if position.IsValid() { - s.errorLogger.Printf("[%s] REF_NOT_FOUND: Ref %q: %s: %s", s.s.Lang(), ref, position.String(), what) + s.errorLogger.Logf("[%s] REF_NOT_FOUND: Ref %q: %s: %s", s.s.Lang(), ref, position.String(), what) } else if p == nil { - s.errorLogger.Printf("[%s] REF_NOT_FOUND: Ref %q: %s", s.s.Lang(), ref, what) + s.errorLogger.Logf("[%s] REF_NOT_FOUND: Ref %q: %s", s.s.Lang(), ref, what) } else { - s.errorLogger.Printf("[%s] REF_NOT_FOUND: Ref %q from page %q: %s", s.s.Lang(), ref, p.Pathc(), what) + s.errorLogger.Logf("[%s] REF_NOT_FOUND: Ref %q from page %q: %s", s.s.Lang(), ref, p.Pathc(), what) } } @@ -507,9 +507,6 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro i18nChanged bool sourceFilesChanged = make(map[string]bool) - - // prevent spamming the log on changes - logger = helpers.NewDistinctErrorLogger() ) var cacheBusters []func(string) bool @@ -531,7 +528,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro switch id.Type { case files.ComponentFolderContent: - logger.Println("Source changed", ev) + s.Log.Println("Source changed", ev) sourceChanged = append(sourceChanged, ev) case files.ComponentFolderLayouts: tmplChanged = true @@ -539,16 +536,16 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro tmplAdded = true } if tmplAdded { - logger.Println("Template added", ev) + s.Log.Println("Template added", ev) } else { - logger.Println("Template changed", ev) + s.Log.Println("Template changed", ev) } case files.ComponentFolderData: - logger.Println("Data changed", ev) + s.Log.Println("Data changed", ev) dataChanged = true case files.ComponentFolderI18n: - logger.Println("i18n changed", ev) + s.Log.Println("i18n changed", ev) i18nChanged = true } diff --git a/hugolib/site_new.go b/hugolib/site_new.go index 911414121..c6c5cd2a3 100644 --- a/hugolib/site_new.go +++ b/hugolib/site_new.go @@ -18,10 +18,12 @@ import ( "errors" "fmt" "html/template" + "os" "sort" "time" radix "github.com/armon/go-radix" + "github.com/bep/logg" "github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/maps" @@ -100,19 +102,41 @@ func (s *Site) Debug() { func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { conf := cfg.Configs.GetFirstLanguageConfig() - logger := cfg.Logger - if logger == nil { - logger = loggers.NewErrorLogger() + var logger loggers.Logger + if cfg.TestLogger != nil { + logger = cfg.TestLogger + } else { + var logHookLast func(e *logg.Entry) error + if cfg.Configs.Base.PanicOnWarning { + logHookLast = loggers.PanicOnWarningHook + } + if cfg.LogOut == nil { + cfg.LogOut = os.Stdout + } + if cfg.LogLevel == 0 { + cfg.LogLevel = logg.LevelWarn + } + + logOpts := loggers.Options{ + Level: cfg.LogLevel, + Distinct: true, // This will drop duplicate log warning and errors. + HandlerPost: logHookLast, + Stdout: cfg.LogOut, + Stderr: cfg.LogOut, + StoreErrors: conf.Running(), + SuppresssStatements: conf.IgnoredErrors(), + } + logger = loggers.New(logOpts) } - ignorableLogger := loggers.NewIgnorableLogger(logger, conf.IgnoredErrors()) firstSiteDeps := &deps.Deps{ Fs: cfg.Fs, - Log: ignorableLogger, + Log: logger, Conf: conf, TemplateProvider: tplimpl.DefaultTemplateProvider, TranslationProvider: i18n.NewTranslationProvider(), } + if err := firstSiteDeps.Init(); err != nil { return nil, err } @@ -128,7 +152,7 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { k := language.Lang conf := confm.LanguageConfigMap[k] - frontmatterHandler, err := pagemeta.NewFrontmatterHandler(cfg.Logger, conf.Frontmatter) + frontmatterHandler, err := pagemeta.NewFrontmatterHandler(firstSiteDeps.Log, conf.Frontmatter) if err != nil { return nil, err } @@ -209,6 +233,7 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { } return h, err + } func newHugoSitesNew(cfg deps.DepsCfg, d *deps.Deps, sites []*Site) (*HugoSites, error) { diff --git a/hugolib/site_render.go b/hugolib/site_render.go index f076b98dd..c4c3f389b 100644 --- a/hugolib/site_render.go +++ b/hugolib/site_render.go @@ -181,7 +181,7 @@ func (s *Site) logMissingLayout(name, layout, kind, outputFormat string) { msg += ": " + errMsg - log.Printf(msg, args...) + log.Logf(msg, args...) } // renderPaginator must be run after the owning Page has been rendered. diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go index 7cf1a5596..e1fd537a8 100644 --- a/hugolib/testhelpers_test.go +++ b/hugolib/testhelpers_test.go @@ -33,6 +33,7 @@ import ( "github.com/fsnotify/fsnotify" "github.com/gohugoio/hugo/common/hexec" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/deps" @@ -47,7 +48,6 @@ import ( "github.com/gohugoio/hugo/resources/resource" qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/hugofs" ) @@ -552,7 +552,7 @@ func (s *sitesBuilder) CreateSitesE() error { if depsCfg.Configs.IsZero() { depsCfg.Configs = s.Configs } - depsCfg.Logger = s.logger + depsCfg.TestLogger = s.logger sites, err := NewHugoSites(depsCfg) diff --git a/langs/i18n/i18n.go b/langs/i18n/i18n.go index a9b7b4c97..1ebd039cd 100644 --- a/langs/i18n/i18n.go +++ b/langs/i18n/i18n.go @@ -24,7 +24,6 @@ import ( "github.com/gohugoio/hugo/common/hreflect" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/go-i18n/v2/i18n" @@ -32,8 +31,6 @@ import ( type translateFunc func(ctx context.Context, translationID string, templateData any) string -var i18nWarningLogger = helpers.NewDistinctErrorLogger() - // Translator handles i18n translations. type Translator struct { translateFuncs map[string]translateFunc @@ -123,7 +120,7 @@ func (t Translator) initFuncs(bndl *i18n.Bundle) { } if t.cfg.LogI18nWarnings() { - i18nWarningLogger.Printf("i18n|MISSING_TRANSLATION|%s|%s", currentLangStr, translationID) + t.logger.Warnf("i18n|MISSING_TRANSLATION|%s|%s", currentLangStr, translationID) } if enableMissingTranslationPlaceholders { diff --git a/langs/i18n/i18n_test.go b/langs/i18n/i18n_test.go index 1ac6144dd..8629c35fc 100644 --- a/langs/i18n/i18n_test.go +++ b/langs/i18n/i18n_test.go @@ -19,12 +19,13 @@ import ( "path/filepath" "testing" + "github.com/bep/logg" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/config/testconfig" "github.com/gohugoio/hugo/tpl/tplimpl" - "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/resources/page" "github.com/spf13/afero" @@ -34,7 +35,7 @@ import ( "github.com/gohugoio/hugo/config" ) -var logger = loggers.NewErrorLogger() +var logger = loggers.NewDefault() type i18nTest struct { name string @@ -406,7 +407,7 @@ other = "{{ . }} miesiąca" for _, variant := range test.variants { c.Assert(f(ctx, test.id, variant.Key), qt.Equals, variant.Value, qt.Commentf("input: %v", variant.Key)) - c.Assert(int(d.Log.LogCounters().WarnCounter.Count()), qt.Equals, 0) + c.Assert(d.Log.LoggCount(logg.LevelWarn), qt.Equals, 0) } }) diff --git a/markup/asciidocext/convert_test.go b/markup/asciidocext/convert_test.go index cdc981263..459686139 100644 --- a/markup/asciidocext/convert_test.go +++ b/markup/asciidocext/convert_test.go @@ -44,7 +44,7 @@ func TestAsciidoctorDefaultArgs(t *testing.T) { p, err := asciidocext.Provider.New( converter.ProviderConfig{ Conf: conf, - Logger: loggers.NewErrorLogger(), + Logger: loggers.NewDefault(), }, ) c.Assert(err, qt.IsNil) @@ -76,7 +76,7 @@ func TestAsciidoctorNonDefaultArgs(t *testing.T) { p, err := asciidocext.Provider.New( converter.ProviderConfig{ Conf: conf, - Logger: loggers.NewErrorLogger(), + Logger: loggers.NewDefault(), }, ) c.Assert(err, qt.IsNil) @@ -106,7 +106,7 @@ func TestAsciidoctorDisallowedArgs(t *testing.T) { p, err := asciidocext.Provider.New( converter.ProviderConfig{ Conf: conf, - Logger: loggers.NewErrorLogger(), + Logger: loggers.NewDefault(), }, ) c.Assert(err, qt.IsNil) @@ -130,7 +130,7 @@ func TestAsciidoctorArbitraryExtension(t *testing.T) { p, err := asciidocext.Provider.New( converter.ProviderConfig{ Conf: conf, - Logger: loggers.NewErrorLogger(), + Logger: loggers.NewDefault(), }, ) c.Assert(err, qt.IsNil) @@ -164,7 +164,7 @@ func TestAsciidoctorDisallowedExtension(t *testing.T) { p, err := asciidocext.Provider.New( converter.ProviderConfig{ Conf: conf, - Logger: loggers.NewErrorLogger(), + Logger: loggers.NewDefault(), }, ) c.Assert(err, qt.IsNil) @@ -195,7 +195,7 @@ trace = false p, err := asciidocext.Provider.New( converter.ProviderConfig{ Conf: conf, - Logger: loggers.NewErrorLogger(), + Logger: loggers.NewDefault(), }, ) c.Assert(err, qt.IsNil) @@ -232,7 +232,7 @@ extensions = ["asciidoctor-html5s", "asciidoctor-diagram"] p, err := asciidocext.Provider.New( converter.ProviderConfig{ Conf: conf, - Logger: loggers.NewErrorLogger(), + Logger: loggers.NewDefault(), }, ) c.Assert(err, qt.IsNil) @@ -272,7 +272,7 @@ my-attribute-name = "my value" p, err := asciidocext.Provider.New( converter.ProviderConfig{ Conf: conf, - Logger: loggers.NewErrorLogger(), + Logger: loggers.NewDefault(), }, ) c.Assert(err, qt.IsNil) @@ -311,7 +311,7 @@ allow = ['asciidoctor'] p, err := asciidocext.Provider.New( converter.ProviderConfig{ - Logger: loggers.NewErrorLogger(), + Logger: loggers.NewDefault(), Conf: conf, Exec: hexec.New(securityConfig), }, diff --git a/markup/goldmark/convert_test.go b/markup/goldmark/convert_test.go index 666529877..a2f7b9597 100644 --- a/markup/goldmark/convert_test.go +++ b/markup/goldmark/convert_test.go @@ -46,7 +46,7 @@ noclasses=false func convert(c *qt.C, conf config.AllProvider, content string) converter.ResultRender { pconf := converter.ProviderConfig{ - Logger: loggers.NewErrorLogger(), + Logger: loggers.NewDefault(), Conf: conf, } @@ -442,7 +442,7 @@ LINE5 conf := testconfig.GetTestConfig(nil, cfg) pcfg := converter.ProviderConfig{ Conf: conf, - Logger: loggers.NewErrorLogger(), + Logger: loggers.NewDefault(), } p, err := goldmark.Provider.New( pcfg, diff --git a/markup/goldmark/toc_test.go b/markup/goldmark/toc_test.go index 78811cfb4..f7f7bb7a0 100644 --- a/markup/goldmark/toc_test.go +++ b/markup/goldmark/toc_test.go @@ -17,12 +17,11 @@ import ( "strings" "testing" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/config/testconfig" "github.com/gohugoio/hugo/markup/converter/hooks" "github.com/gohugoio/hugo/markup/goldmark" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/markup/converter" qt "github.com/frankban/quicktest" @@ -56,7 +55,7 @@ And then some. p, err := goldmark.Provider.New( converter.ProviderConfig{ Conf: testconfig.GetTestConfig(nil, nil), - Logger: loggers.NewErrorLogger(), + Logger: loggers.NewDefault(), }) c.Assert(err, qt.IsNil) conv, err := p.New(converter.DocumentContext{}) @@ -86,12 +85,12 @@ func TestEscapeToc(t *testing.T) { safeP, _ := goldmark.Provider.New( converter.ProviderConfig{ Conf: safeConf(), - Logger: loggers.NewErrorLogger(), + Logger: loggers.NewDefault(), }) unsafeP, _ := goldmark.Provider.New( converter.ProviderConfig{ Conf: unsafeConf(), - Logger: loggers.NewErrorLogger(), + Logger: loggers.NewDefault(), }) safeConv, _ := safeP.New(converter.DocumentContext{}) unsafeConv, _ := unsafeP.New(converter.DocumentContext{}) diff --git a/markup/org/convert.go b/markup/org/convert.go index 802b9aa5a..141269f1d 100644 --- a/markup/org/convert.go +++ b/markup/org/convert.go @@ -16,6 +16,7 @@ package org import ( "bytes" + "log" "github.com/gohugoio/hugo/identity" @@ -46,7 +47,7 @@ type orgConverter struct { func (c *orgConverter) Convert(ctx converter.RenderContext) (converter.ResultRender, error) { logger := c.cfg.Logger config := org.New() - config.Log = logger.Warn() + config.Log = log.Default() // TODO(bep) config.ReadFile = func(filename string) ([]byte, error) { return afero.ReadFile(c.cfg.ContentFs, filename) } diff --git a/markup/org/convert_test.go b/markup/org/convert_test.go index 08841b2d7..1422585af 100644 --- a/markup/org/convert_test.go +++ b/markup/org/convert_test.go @@ -29,7 +29,7 @@ import ( func TestConvert(t *testing.T) { c := qt.New(t) p, err := org.Provider.New(converter.ProviderConfig{ - Logger: loggers.NewErrorLogger(), + Logger: loggers.NewDefault(), Conf: testconfig.GetTestConfig(afero.NewMemMapFs(), nil), }) c.Assert(err, qt.IsNil) diff --git a/markup/pandoc/convert_test.go b/markup/pandoc/convert_test.go index f549d5f4f..6a1535946 100644 --- a/markup/pandoc/convert_test.go +++ b/markup/pandoc/convert_test.go @@ -32,7 +32,7 @@ func TestConvert(t *testing.T) { c := qt.New(t) sc := security.DefaultConfig sc.Exec.Allow = security.NewWhitelist("pandoc") - p, err := Provider.New(converter.ProviderConfig{Exec: hexec.New(sc), Logger: loggers.NewErrorLogger()}) + p, err := Provider.New(converter.ProviderConfig{Exec: hexec.New(sc), Logger: loggers.NewDefault()}) c.Assert(err, qt.IsNil) conv, err := p.New(converter.DocumentContext{}) c.Assert(err, qt.IsNil) diff --git a/markup/rst/convert_test.go b/markup/rst/convert_test.go index 5d2882de1..9e98d0405 100644 --- a/markup/rst/convert_test.go +++ b/markup/rst/convert_test.go @@ -35,7 +35,7 @@ func TestConvert(t *testing.T) { p, err := Provider.New( converter.ProviderConfig{ - Logger: loggers.NewErrorLogger(), + Logger: loggers.NewDefault(), Exec: hexec.New(sc), }) c.Assert(err, qt.IsNil) diff --git a/modules/client.go b/modules/client.go index 59f6b25d3..5d8daf926 100644 --- a/modules/client.go +++ b/modules/client.go @@ -30,6 +30,7 @@ import ( "github.com/gohugoio/hugo/common/collections" "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/hexec" + "github.com/gohugoio/hugo/common/loggers" hglob "github.com/gohugoio/hugo/hugofs/glob" @@ -39,8 +40,6 @@ import ( "github.com/gohugoio/hugo/hugofs/files" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/config" "github.com/rogpeppe/go-internal/module" @@ -98,7 +97,7 @@ func NewClient(cfg ClientConfig) *Client { logger := cfg.Logger if logger == nil { - logger = loggers.NewWarningLogger() + logger = loggers.NewDefault() } var noVendor glob.Glob diff --git a/parser/metadecoders/decoder.go b/parser/metadecoders/decoder.go index a65731ae4..93eb32e47 100644 --- a/parser/metadecoders/decoder.go +++ b/parser/metadecoders/decoder.go @@ -18,6 +18,7 @@ import ( "encoding/csv" "encoding/json" "fmt" + "log" "regexp" "strings" @@ -28,7 +29,6 @@ import ( toml "github.com/pelletier/go-toml/v2" "github.com/spf13/afero" "github.com/spf13/cast" - jww "github.com/spf13/jwalterweatherman" yaml "gopkg.in/yaml.v2" ) @@ -231,7 +231,7 @@ func parseORGDate(s string) string { func (d Decoder) unmarshalORG(data []byte, v any) error { config := org.New() - config.Log = jww.WARN + config.Log = log.Default() // TODO(bep) document := config.Parse(bytes.NewReader(data), "") if document.Error != nil { return document.Error @@ -242,7 +242,7 @@ func (d Decoder) unmarshalORG(data []byte, v any) error { if strings.HasSuffix(k, "[]") { frontMatter[k[:len(k)-2]] = strings.Fields(v) } else if k == "tags" || k == "categories" || k == "aliases" { - jww.WARN.Printf("Please use '#+%s[]:' notation, automatic conversion is deprecated.", k) + log.Printf("warn: Please use '#+%s[]:' notation, automatic conversion is deprecated.", k) frontMatter[k] = strings.Fields(v) } else if k == "date" || k == "lastmod" || k == "publishdate" || k == "expirydate" { frontMatter[k] = parseORGDate(v) diff --git a/resources/page/pagemeta/page_frontmatter.go b/resources/page/pagemeta/page_frontmatter.go index d827bfbad..98ab6b222 100644 --- a/resources/page/pagemeta/page_frontmatter.go +++ b/resources/page/pagemeta/page_frontmatter.go @@ -18,9 +18,9 @@ import ( "time" "github.com/gohugoio/hugo/common/htime" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/paths" - "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/resources/resource" @@ -270,7 +270,7 @@ func toLowerSlice(in any) []string { // If no logger is provided, one will be created. func NewFrontmatterHandler(logger loggers.Logger, frontMatterConfig FrontmatterConfig) (FrontMatterHandler, error) { if logger == nil { - logger = loggers.NewErrorLogger() + logger = loggers.NewDefault() } allDateKeys := make(map[string]bool) diff --git a/resources/page/testhelpers_page_test.go b/resources/page/testhelpers_page_test.go index c462e176f..95124cb58 100644 --- a/resources/page/testhelpers_page_test.go +++ b/resources/page/testhelpers_page_test.go @@ -30,7 +30,7 @@ func newTestPathSpecFor(cfg config.Provider) *helpers.PathSpec { mfs := afero.NewMemMapFs() conf := testconfig.GetTestConfig(mfs, cfg) fs := hugofs.NewFrom(mfs, conf.BaseConfig()) - ps, err := helpers.NewPathSpec(fs, conf, loggers.NewErrorLogger()) + ps, err := helpers.NewPathSpec(fs, conf, loggers.NewDefault()) if err != nil { panic(err) } diff --git a/resources/resource_spec.go b/resources/resource_spec.go index 5ecb021fe..3e1b53205 100644 --- a/resources/resource_spec.go +++ b/resources/resource_spec.go @@ -31,6 +31,7 @@ import ( "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/hexec" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/identity" @@ -39,7 +40,6 @@ import ( "github.com/gohugoio/hugo/resources/postpub" "github.com/gohugoio/hugo/cache/filecache" - "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/resources/images" "github.com/gohugoio/hugo/resources/page" @@ -75,7 +75,7 @@ func NewSpec( } if logger == nil { - logger = loggers.NewErrorLogger() + logger = loggers.NewDefault() } permalinks, err := page.NewPermalinkExpander(s.URLize, conf.Permalinks) diff --git a/resources/resource_transformers/babel/babel.go b/resources/resource_transformers/babel/babel.go index ff19d9dda..5f8fcb00f 100644 --- a/resources/resource_transformers/babel/babel.go +++ b/resources/resource_transformers/babel/babel.go @@ -122,10 +122,10 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx } var configFile string - logger := t.rs.Logger + infol := t.rs.Logger.InfoCommand(binaryName) + infoW := loggers.LevelLoggerToWriter(infol) var errBuf bytes.Buffer - infoW := loggers.LoggerToWriterWithPrefix(logger.Info(), "babel") if t.options.Config != "" { configFile = t.options.Config @@ -149,7 +149,7 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx var cmdArgs []any if configFile != "" { - logger.Infoln("babel: use config file", configFile) + infol.Logf("use config file %q", configFile) cmdArgs = []any{"--config-file", configFile} } diff --git a/resources/resource_transformers/babel/integration_test.go b/resources/resource_transformers/babel/integration_test.go index 164e7fd40..44a13f103 100644 --- a/resources/resource_transformers/babel/integration_test.go +++ b/resources/resource_transformers/babel/integration_test.go @@ -16,8 +16,7 @@ package babel_test import ( "testing" - jww "github.com/spf13/jwalterweatherman" - + "github.com/bep/logg" "github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/hugolib" ) @@ -82,7 +81,7 @@ Transpiled3: {{ $transpiled.Permalink }} TxtarString: files, NeedsOsFS: true, NeedsNpmInstall: true, - LogLevel: jww.LevelInfo, + LogLevel: logg.LevelInfo, }).Build() b.AssertLogContains("babel: Hugo Environment: production") diff --git a/resources/resource_transformers/postcss/integration_test.go b/resources/resource_transformers/postcss/integration_test.go index cfe5f8a2c..c920fe17d 100644 --- a/resources/resource_transformers/postcss/integration_test.go +++ b/resources/resource_transformers/postcss/integration_test.go @@ -16,11 +16,11 @@ package postcss_test import ( "fmt" "path/filepath" + "runtime" "strings" "testing" - jww "github.com/spf13/jwalterweatherman" - + "github.com/bep/logg" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/hugofs" @@ -124,7 +124,7 @@ func TestTransformPostCSS(t *testing.T) { T: c, NeedsOsFS: true, NeedsNpmInstall: true, - LogLevel: jww.LevelInfo, + LogLevel: logg.LevelInfo, WorkingDir: tempDir, TxtarString: files, }).Build() @@ -148,6 +148,11 @@ func TestTransformPostCSSError(t *testing.T) { t.Skip("Skip long running test when running locally") } + if runtime.GOOS == "windows" { + //TODO(bep) This has started to fail on Windows with Go 1.19 on GitHub Actions for some mysterious reason. + t.Skip("Skip on Windows") + } + c := qt.New(t) s, err := hugolib.NewIntegrationTestBuilder( @@ -176,7 +181,7 @@ func TestTransformPostCSSImportError(t *testing.T) { T: c, NeedsOsFS: true, NeedsNpmInstall: true, - LogLevel: jww.LevelInfo, + LogLevel: logg.LevelInfo, TxtarString: strings.ReplaceAll(postCSSIntegrationTestFiles, `@import "components/all.css";`, `@import "components/doesnotexist.css";`), }).BuildE() @@ -201,7 +206,7 @@ func TestTransformPostCSSImporSkipInlineImportsNotFound(t *testing.T) { T: c, NeedsOsFS: true, NeedsNpmInstall: true, - LogLevel: jww.LevelInfo, + LogLevel: logg.LevelInfo, TxtarString: files, }).Build() @@ -233,7 +238,7 @@ func TestTransformPostCSSResourceCacheWithPathInBaseURL(t *testing.T) { T: c, NeedsOsFS: true, NeedsNpmInstall: true, - LogLevel: jww.LevelInfo, + LogLevel: logg.LevelInfo, TxtarString: files, WorkingDir: tempDir, }).Build() diff --git a/resources/resource_transformers/postcss/postcss.go b/resources/resource_transformers/postcss/postcss.go index 376d72182..083607246 100644 --- a/resources/resource_transformers/postcss/postcss.go +++ b/resources/resource_transformers/postcss/postcss.go @@ -27,13 +27,12 @@ import ( "github.com/gohugoio/hugo/common/collections" "github.com/gohugoio/hugo/common/hexec" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/text" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/common/hugo" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/resources/internal" "github.com/spf13/afero" "github.com/spf13/cast" @@ -151,10 +150,12 @@ func (t *postcssTransformation) Key() internal.ResourceTransformationKey { func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationCtx) error { const binaryName = "postcss" + infol := t.rs.Logger.InfoCommand(binaryName) + infoW := loggers.LevelLoggerToWriter(infol) + ex := t.rs.ExecHelper var configFile string - logger := t.rs.Logger var options Options if t.optionsm != nil { @@ -185,7 +186,7 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC var cmdArgs []any if configFile != "" { - logger.Infoln("postcss: use config file", configFile) + infol.Logf("use config file %q", configFile) cmdArgs = []any{"--config", configFile} } @@ -194,7 +195,6 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC } var errBuf bytes.Buffer - infoW := loggers.LoggerToWriterWithPrefix(logger.Info(), "postcss") stderr := io.MultiWriter(infoW, &errBuf) cmdArgs = append(cmdArgs, hexec.WithStderr(stderr)) @@ -401,7 +401,6 @@ func (imp *importResolver) shouldImport(s string) bool { } func (imp *importResolver) toFileError(output string) error { - output = strings.TrimSpace(loggers.RemoveANSIColours(output)) inErr := errors.New(output) match := cssSyntaxErrorRe.FindStringSubmatch(output) diff --git a/resources/resource_transformers/postcss/postcss_test.go b/resources/resource_transformers/postcss/postcss_test.go index 6901d69de..dd0695cd1 100644 --- a/resources/resource_transformers/postcss/postcss_test.go +++ b/resources/resource_transformers/postcss/postcss_test.go @@ -18,9 +18,9 @@ import ( "strings" "testing" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/htesting/hqt" - "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/helpers" "github.com/spf13/afero" @@ -95,7 +95,7 @@ LOCAL_STYLE mainStyles, "styles.css", Options{}, - fs, loggers.NewErrorLogger(), + fs, loggers.NewDefault(), ) r, err := imp.resolve() @@ -144,7 +144,7 @@ LOCAL_STYLE @import "e.css"; @import "missing.css";` - logger := loggers.NewErrorLogger() + logger := loggers.NewDefault() for i := 0; i < b.N; i++ { b.StopTimer() diff --git a/resources/resource_transformers/tocss/dartsass/client.go b/resources/resource_transformers/tocss/dartsass/client.go index 9ae317886..63278f0db 100644 --- a/resources/resource_transformers/tocss/dartsass/client.go +++ b/resources/resource_transformers/tocss/dartsass/client.go @@ -58,6 +58,8 @@ func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) (*Client, error) transpiler *godartsass.Transpiler transpilerv1 *godartsassv1.Transpiler err error + infol = rs.Logger.InfoCommand("Dart Sass") + warnl = rs.Logger.WarnCommand("Dart Sass") ) if hugo.IsDartSassV2() { @@ -68,10 +70,10 @@ func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) (*Client, error) switch event.Type { case godartsass.LogEventTypeDebug: // Log as Info for now, we may adjust this if it gets too chatty. - rs.Logger.Infof("Dart Sass: %s", message) + infol.Logf(message) default: // The rest are either deprecations or @warn statements. - rs.Logger.Warnf("Dart Sass: %s", message) + warnl.Logf(message) } }, }) @@ -84,10 +86,10 @@ func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) (*Client, error) switch event.Type { case godartsassv1.LogEventTypeDebug: // Log as Info for now, we may adjust this if it gets too chatty. - rs.Logger.Infof("Dart Sass: %s", message) + infol.Logf(message) default: // The rest are either deprecations or @warn statements. - rs.Logger.Warnf("Dart Sass: %s", message) + warnl.Logf(message) } }, }) diff --git a/resources/resource_transformers/tocss/dartsass/integration_test.go b/resources/resource_transformers/tocss/dartsass/integration_test.go index d76592336..c370a1cc8 100644 --- a/resources/resource_transformers/tocss/dartsass/integration_test.go +++ b/resources/resource_transformers/tocss/dartsass/integration_test.go @@ -17,10 +17,10 @@ import ( "strings" "testing" + "github.com/bep/logg" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass" - jww "github.com/spf13/jwalterweatherman" ) func TestTransformIncludePaths(t *testing.T) { @@ -288,11 +288,11 @@ T1: {{ $r.Content }} T: t, TxtarString: files, NeedsOsFS: true, - LogLevel: jww.LevelInfo, + LogLevel: logg.LevelInfo, }).Build() - b.AssertLogMatches(`WARN.*Dart Sass: foo`) - b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:1:0: bar`) + b.AssertLogMatches(`Dart Sass: foo`) + b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:1:0: bar`) } @@ -513,20 +513,20 @@ T1: {{ $r.Content }} T: t, TxtarString: files, NeedsOsFS: true, - LogLevel: jww.LevelInfo, + LogLevel: logg.LevelInfo, }).Build() - b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:3:0: color`) - b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:4:0: color`) - b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:5:0: color`) - b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:6:0: number`) - b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:7:0: number`) - b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:8:0: number`) - b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:9:0: string`) - b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:10:0: string`) - b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:11:0: string`) - b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:12:0: number`) - b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:13:0: number`) - b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:14:0: number`) + b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:3:0: color`) + b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:4:0: color`) + b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:5:0: color`) + b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:6:0: number`) + b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:7:0: number`) + b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:8:0: number`) + b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:9:0: string`) + b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:10:0: string`) + b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:11:0: string`) + b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:12:0: number`) + b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:13:0: number`) + b.AssertLogMatches(`Dart Sass: .*assets.*main.scss:14:0: number`) } diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go index 35a87394a..04b777dfb 100644 --- a/tpl/collections/collections.go +++ b/tpl/collections/collections.go @@ -30,7 +30,6 @@ import ( "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/tpl/compare" "github.com/spf13/cast" @@ -393,7 +392,7 @@ func (ns *Namespace) IsSet(c any, key any) (bool, error) { return av.MapIndex(kv).IsValid(), nil } default: - helpers.DistinctErrorLog.Printf("WARNING: calling IsSet with unsupported type %q (%T) will always return false.\n", av.Kind(), c) + ns.deps.Log.Warnf("calling IsSet with unsupported type %q (%T) will always return false.\n", av.Kind(), c) } return false, nil diff --git a/tpl/data/data.go b/tpl/data/data.go index 251cf1a4f..380c25685 100644 --- a/tpl/data/data.go +++ b/tpl/data/data.go @@ -30,7 +30,6 @@ import ( "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/constants" - "github.com/gohugoio/hugo/common/loggers" "github.com/spf13/cast" @@ -92,7 +91,7 @@ func (ns *Namespace) GetCSV(sep string, args ...any) (d [][]string, err error) { if security.IsAccessDenied(err) { return nil, err } - ns.deps.Log.(loggers.IgnorableLogger).Errorsf(constants.ErrRemoteGetCSV, "Failed to get CSV resource %q: %s", url, err) + ns.deps.Log.Errorsf(constants.ErrRemoteGetCSV, "Failed to get CSV resource %q: %s", url, err) return nil, nil } @@ -128,7 +127,7 @@ func (ns *Namespace) GetJSON(args ...any) (any, error) { if security.IsAccessDenied(err) { return nil, err } - ns.deps.Log.(loggers.IgnorableLogger).Errorsf(constants.ErrRemoteGetJSON, "Failed to get JSON resource %q: %s", url, err) + ns.deps.Log.Errorsf(constants.ErrRemoteGetJSON, "Failed to get JSON resource %q: %s", url, err) return nil, nil } diff --git a/tpl/data/data_test.go b/tpl/data/data_test.go index f10b88a32..c51dbbd82 100644 --- a/tpl/data/data_test.go +++ b/tpl/data/data_test.go @@ -22,6 +22,7 @@ import ( "strings" "testing" + "github.com/bep/logg" "github.com/gohugoio/hugo/common/maps" qt "github.com/frankban/quicktest" @@ -108,13 +109,13 @@ func TestGetCSV(t *testing.T) { got, err := ns.GetCSV(test.sep, test.url) if _, ok := test.expect.(bool); ok { - c.Assert(int(ns.deps.Log.LogCounters().ErrorCounter.Count()), qt.Equals, 1) + c.Assert(int(ns.deps.Log.LoggCount(logg.LevelError)), qt.Equals, 1) c.Assert(got, qt.IsNil) return } c.Assert(err, qt.IsNil, msg) - c.Assert(int(ns.deps.Log.LogCounters().ErrorCounter.Count()), qt.Equals, 0) + c.Assert(int(ns.deps.Log.LoggCount(logg.LevelError)), qt.Equals, 0) c.Assert(got, qt.Not(qt.IsNil), msg) c.Assert(got, qt.DeepEquals, test.expect, msg) }) @@ -200,11 +201,11 @@ func TestGetJSON(t *testing.T) { got, _ := ns.GetJSON(test.url) if _, ok := test.expect.(bool); ok { - c.Assert(int(ns.deps.Log.LogCounters().ErrorCounter.Count()), qt.Equals, 1) + c.Assert(int(ns.deps.Log.LoggCount(logg.LevelError)), qt.Equals, 1) return } - c.Assert(int(ns.deps.Log.LogCounters().ErrorCounter.Count()), qt.Equals, 0, msg) + c.Assert(int(ns.deps.Log.LoggCount(logg.LevelError)), qt.Equals, 0, msg) c.Assert(got, qt.Not(qt.IsNil), msg) c.Assert(got, qt.DeepEquals, test.expect) @@ -283,7 +284,7 @@ func TestHeaders(t *testing.T) { err := fn("http://example.org/api", "?foo", test.headers) c.Assert(err, qt.IsNil) - c.Assert(int(ns.deps.Log.LogCounters().ErrorCounter.Count()), qt.Equals, 0) + c.Assert(int(ns.deps.Log.LoggCount(logg.LevelError)), qt.Equals, 0) test.assert(c, headers.String()) } diff --git a/tpl/data/resources_test.go b/tpl/data/resources_test.go index ad4ab20f4..d452a2a43 100644 --- a/tpl/data/resources_test.go +++ b/tpl/data/resources_test.go @@ -15,6 +15,9 @@ package data import ( "bytes" + + "github.com/gohugoio/hugo/common/loggers" + "net/http" "net/http/httptest" "net/url" @@ -29,7 +32,6 @@ import ( qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/cache/filecache" - "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/hugofs" @@ -182,7 +184,7 @@ func TestScpGetRemoteParallel(t *testing.T) { func newDeps(cfg config.Provider) *deps.Deps { conf := testconfig.GetTestConfig(nil, cfg) - logger := loggers.NewIgnorableLogger(loggers.NewErrorLogger(), nil) + logger := loggers.NewDefault() fs := hugofs.NewFrom(afero.NewMemMapFs(), conf.BaseConfig()) d := &deps.Deps{ diff --git a/tpl/fmt/fmt.go b/tpl/fmt/fmt.go index 0667bcedd..c0d75e425 100644 --- a/tpl/fmt/fmt.go +++ b/tpl/fmt/fmt.go @@ -16,27 +16,22 @@ package fmt import ( _fmt "fmt" + "sort" + "github.com/bep/logg" "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/helpers" + "github.com/spf13/cast" ) // New returns a new instance of the fmt-namespaced template functions. func New(d *deps.Deps) *Namespace { - ignorableLogger, ok := d.Log.(loggers.IgnorableLogger) - if !ok { - ignorableLogger = loggers.NewIgnorableLogger(d.Log, nil) - } - - distinctLogger := helpers.NewDistinctLogger(d.Log) ns := &Namespace{ - distinctLogger: ignorableLogger.Apply(distinctLogger), + logger: d.Log, } d.BuildStartListeners.Add(func() { - ns.distinctLogger.Reset() + ns.logger.Reset() }) return ns @@ -44,7 +39,7 @@ func New(d *deps.Deps) *Namespace { // Namespace provides template functions for the "fmt" namespace. type Namespace struct { - distinctLogger loggers.IgnorableLogger + logger loggers.Logger } // Print returns a string representation of args. @@ -65,7 +60,7 @@ func (ns *Namespace) Println(args ...any) string { // Errorf formats args according to a format specifier and logs an ERROR. // It returns an empty string. func (ns *Namespace) Errorf(format string, args ...any) string { - ns.distinctLogger.Errorf(format, args...) + ns.logger.Errorf(format, args...) return "" } @@ -73,13 +68,41 @@ func (ns *Namespace) Errorf(format string, args ...any) string { // an information text that the error with the given id can be suppressed in config. // It returns an empty string. func (ns *Namespace) Erroridf(id, format string, args ...any) string { - ns.distinctLogger.Errorsf(id, format, args...) + ns.logger.Errorsf(id, format, args...) return "" } // Warnf formats args according to a format specifier and logs a WARNING. // It returns an empty string. func (ns *Namespace) Warnf(format string, args ...any) string { - ns.distinctLogger.Warnf(format, args...) + ns.logger.Warnf(format, args...) + return "" +} + +// Warnmf is epxermimental and subject to change at any time. +func (ns *Namespace) Warnmf(m any, format string, args ...any) string { + return ns.logmf(ns.logger.Warn(), m, format, args...) +} + +// Errormf is epxermimental and subject to change at any time. +func (ns *Namespace) Errormf(m any, format string, args ...any) string { + return ns.logmf(ns.logger.Error(), m, format, args...) +} + +func (ns *Namespace) logmf(l logg.LevelLogger, m any, format string, args ...any) string { + mm := cast.ToStringMap(m) + fields := make(logg.Fields, len(mm)) + i := 0 + for k, v := range mm { + fields[i] = logg.Field{Name: k, Value: v} + i++ + } + // Sort the fields to make the output deterministic. + sort.Slice(fields, func(i, j int) bool { + return fields[i].Name < fields[j].Name + }) + + l.WithFields(fields).Logf(format, args...) + return "" } diff --git a/transform/livereloadinject/livereloadinject.go b/transform/livereloadinject/livereloadinject.go index d57ba4d24..a29a64ebb 100644 --- a/transform/livereloadinject/livereloadinject.go +++ b/transform/livereloadinject/livereloadinject.go @@ -20,7 +20,8 @@ import ( "net/url" "strings" - "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/transform" ) @@ -90,7 +91,7 @@ func New(baseURL url.URL) transform.Transformer { c = append(c[:i], append(script, c[i:]...)...) if _, err := ft.To().Write(c); err != nil { - helpers.DistinctWarnLog.Println("Failed to inject LiveReload script:", err) + loggers.Log().Warnf("Failed to inject LiveReload script:", err) } return nil } diff --git a/transform/metainject/hugogenerator.go b/transform/metainject/hugogenerator.go index fd3a6a4ab..43a477354 100644 --- a/transform/metainject/hugogenerator.go +++ b/transform/metainject/hugogenerator.go @@ -19,7 +19,7 @@ import ( "regexp" "github.com/gohugoio/hugo/common/hugo" - "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/transform" ) @@ -33,7 +33,7 @@ func HugoGenerator(ft transform.FromTo) error { b := ft.From().Bytes() if metaTagsCheck.Match(b) { if _, err := ft.To().Write(b); err != nil { - helpers.DistinctWarnLog.Println("Failed to inject Hugo generator tag:", err) + loggers.Log().Warnf("Failed to inject Hugo generator tag: %s", err) } return nil } @@ -49,7 +49,7 @@ func HugoGenerator(ft transform.FromTo) error { } if _, err := ft.To().Write(newcontent); err != nil { - helpers.DistinctWarnLog.Println("Failed to inject Hugo generator tag:", err) + loggers.Log().Warnf("Failed to inject Hugo generator tag: %s", err) } return nil