Repository: gohugoio/hugo
Pull request #11122 (2023-06-16 14:09:53)

Add structured logging


Patch:
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/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/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/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/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 logging level to use.
+	LogLevel logg.Level
 
-	// The Logger to use.
-	Logger loggers.Logger
+	// 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/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/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/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_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/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/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/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/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/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/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/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/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/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/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/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


Test patch:
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/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/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/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_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/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_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/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/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_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_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_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/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_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_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/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_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_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/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/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{