summaryrefslogtreecommitdiffstats
path: root/common
diff options
context:
space:
mode:
Diffstat (limited to 'common')
-rw-r--r--common/loggers/handlerdefault.go106
-rw-r--r--common/loggers/handlersmisc.go158
-rw-r--r--common/loggers/handlerterminal.go90
-rw-r--r--common/loggers/ignorableLogger.go63
-rw-r--r--common/loggers/logger.go303
-rw-r--r--common/loggers/logger_test.go156
-rw-r--r--common/loggers/loggerglobal.go53
-rw-r--r--common/loggers/loggers.go355
-rw-r--r--common/loggers/loggers_test.go60
9 files changed, 866 insertions, 478 deletions
diff --git a/common/loggers/handlerdefault.go b/common/loggers/handlerdefault.go
new file mode 100644
index 000000000..28b85ed22
--- /dev/null
+++ b/common/loggers/handlerdefault.go
@@ -0,0 +1,106 @@
+// Copyright 2023 The Hugo Authors. All rights reserved.
+// Some functions in this file (see comments) is based on the Go source code,
+// copyright The Go Authors and governed by a BSD-style license.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// package loggers contains some basic logging setup.
+package loggers
+
+import (
+ "fmt"
+ "io"
+ "strings"
+ "sync"
+
+ "github.com/bep/logg"
+
+ "github.com/fatih/color"
+)
+
+var bold = color.New(color.Bold)
+
+// levelColor mapping.
+var levelColor = [...]*color.Color{
+ logg.LevelDebug: color.New(color.FgWhite),
+ logg.LevelInfo: color.New(color.FgBlue),
+ logg.LevelWarn: color.New(color.FgYellow),
+ logg.LevelError: color.New(color.FgRed),
+}
+
+// levelString mapping.
+var levelString = [...]string{
+ logg.LevelDebug: "DEBUG",
+ logg.LevelInfo: "INFO ",
+ logg.LevelWarn: "WARN ",
+ logg.LevelError: "ERROR",
+}
+
+// newDefaultHandler handler.
+func newDefaultHandler(outWriter, errWriter io.Writer) logg.Handler {
+ return &defaultHandler{
+ outWriter: outWriter,
+ errWriter: errWriter,
+ Padding: 0,
+ }
+}
+
+// Default Handler implementation.
+// Based on https://github.com/apex/log/blob/master/handlers/cli/cli.go
+type defaultHandler struct {
+ mu sync.Mutex
+ outWriter io.Writer // Defaults to os.Stdout.
+ errWriter io.Writer // Defaults to os.Stderr.
+
+ Padding int
+}
+
+// HandleLog implements logg.Handler.
+func (h *defaultHandler) HandleLog(e *logg.Entry) error {
+ color := levelColor[e.Level]
+ level := levelString[e.Level]
+
+ h.mu.Lock()
+ defer h.mu.Unlock()
+
+ var w io.Writer
+ if e.Level > logg.LevelInfo {
+ w = h.errWriter
+ } else {
+ w = h.outWriter
+ }
+
+ var prefix string
+ for _, field := range e.Fields {
+ if field.Name == FieldNameCmd {
+ prefix = fmt.Sprint(field.Value)
+ break
+ }
+ }
+
+ if prefix != "" {
+ prefix = prefix + ": "
+ }
+
+ color.Fprintf(w, "%s %s%s", bold.Sprintf("%*s", h.Padding+1, level), color.Sprint(prefix), e.Message)
+
+ for _, field := range e.Fields {
+ if strings.HasPrefix(field.Name, reservedFieldNamePrefix) {
+ continue
+ }
+ fmt.Fprintf(w, " %s %v", color.Sprint(field.Name), field.Value)
+ }
+
+ fmt.Fprintln(w)
+
+ return nil
+}
diff --git a/common/loggers/handlersmisc.go b/common/loggers/handlersmisc.go
new file mode 100644
index 000000000..5c9d6c091
--- /dev/null
+++ b/common/loggers/handlersmisc.go
@@ -0,0 +1,158 @@
+// Copyright 2023 The Hugo Authors. All rights reserved.
+// Some functions in this file (see comments) is based on the Go source code,
+// copyright The Go Authors and governed by a BSD-style license.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package loggers
+
+import (
+ "fmt"
+ "strings"
+ "sync"
+
+ "github.com/bep/logg"
+ "github.com/gohugoio/hugo/identity"
+)
+
+// PanicOnWarningHook panics on warnings.
+var PanicOnWarningHook = func(e *logg.Entry) error {
+ if e.Level != logg.LevelWarn {
+ return nil
+ }
+ panic(e.Message)
+}
+
+func newLogLevelCounter() *logLevelCounter {
+ return &logLevelCounter{
+ counters: make(map[logg.Level]int),
+ }
+}
+
+func newLogOnceHandler(threshold logg.Level) *logOnceHandler {
+ return &logOnceHandler{
+ threshold: threshold,
+ seen: make(map[uint64]bool),
+ }
+}
+
+func newStopHandler(h ...logg.Handler) *stopHandler {
+ return &stopHandler{
+ handlers: h,
+ }
+}
+
+func newSuppressStatementsHandler(statements map[string]bool) *suppressStatementsHandler {
+ return &suppressStatementsHandler{
+ statements: statements,
+ }
+}
+
+type logLevelCounter struct {
+ mu sync.RWMutex
+ counters map[logg.Level]int
+}
+
+func (h *logLevelCounter) HandleLog(e *logg.Entry) error {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+ h.counters[e.Level]++
+ return nil
+}
+
+var stopError = fmt.Errorf("stop")
+
+type logOnceHandler struct {
+ threshold logg.Level
+ mu sync.Mutex
+ seen map[uint64]bool
+}
+
+func (h *logOnceHandler) HandleLog(e *logg.Entry) error {
+ if e.Level < h.threshold {
+ // We typically only want to enable this for warnings and above.
+ // The common use case is that many go routines may log the same error.
+ return nil
+ }
+ h.mu.Lock()
+ defer h.mu.Unlock()
+ hash := identity.HashUint64(e.Level, e.Message, e.Fields)
+ if h.seen[hash] {
+ return stopError
+ }
+ h.seen[hash] = true
+ return nil
+}
+
+func (h *logOnceHandler) reset() {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+ h.seen = make(map[uint64]bool)
+}
+
+type stopHandler struct {
+ handlers []logg.Handler
+}
+
+// HandleLog implements logg.Handler.
+func (h *stopHandler) HandleLog(e *logg.Entry) error {
+ for _, handler := range h.handlers {
+ if err := handler.HandleLog(e); err != nil {
+ if err == stopError {
+ return nil
+ }
+ return err
+ }
+ }
+ return nil
+}
+
+type suppressStatementsHandler struct {
+ statements map[string]bool
+}
+
+func (h *suppressStatementsHandler) HandleLog(e *logg.Entry) error {
+ for _, field := range e.Fields {
+ if field.Name == FieldNameStatementID {
+ if h.statements[field.Value.(string)] {
+ return stopError
+ }
+ }
+ }
+ return nil
+}
+
+// replacer creates a new log handler that does string replacement in log messages.
+func replacer(repl *strings.Replacer) logg.Handler {
+ return logg.HandlerFunc(func(e *logg.Entry) error {
+ e.Message = repl.Replace(e.Message)
+ for i, field := range e.Fields {
+ if s, ok := field.Value.(string); ok {
+ e.Fields[i].Value = repl.Replace(s)
+ }
+ }
+ return nil
+ })
+}
+
+// whiteSpaceTrimmer creates a new log handler that trims whitespace from log messages and string fields.
+func whiteSpaceTrimmer() logg.Handler {
+ return logg.HandlerFunc(func(e *logg.Entry) error {
+ e.Message = strings.TrimSpace(e.Message)
+ for i, field := range e.Fields {
+ if s, ok := field.Value.(string); ok {
+ e.Fields[i].Value = strings.TrimSpace(s)
+ }
+ }
+ return nil
+ })
+}
diff --git a/common/loggers/handlerterminal.go b/common/loggers/handlerterminal.go
new file mode 100644
index 000000000..e3d377bbf
--- /dev/null
+++ b/common/loggers/handlerterminal.go
@@ -0,0 +1,90 @@
+// Copyright 2023 The Hugo Authors. All rights reserved.
+// Some functions in this file (see comments) is based on the Go source code,
+// copyright The Go Authors and governed by a BSD-style license.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package loggers
+
+import (
+ "fmt"
+ "io"
+ "strings"
+ "sync"
+
+ "github.com/bep/logg"
+)
+
+// newNoColoursHandler creates a new NoColoursHandler
+func newNoColoursHandler(outWriter, errWriter io.Writer, noLevelPrefix bool, predicate func(*logg.Entry) bool) *noColoursHandler {
+ if predicate == nil {
+ predicate = func(e *logg.Entry) bool { return true }
+ }
+ return &noColoursHandler{
+ noLevelPrefix: noLevelPrefix,
+ outWriter: outWriter,
+ errWriter: errWriter,
+ predicate: predicate,
+ }
+}
+
+type noColoursHandler struct {
+ mu sync.Mutex
+ outWriter io.Writer // Defaults to os.Stdout.
+ errWriter io.Writer // Defaults to os.Stderr.
+ predicate func(*logg.Entry) bool
+ noLevelPrefix bool
+}
+
+func (h *noColoursHandler) HandleLog(e *logg.Entry) error {
+ if !h.predicate(e) {
+ return nil
+ }
+ h.mu.Lock()
+ defer h.mu.Unlock()
+
+ var w io.Writer
+ if e.Level > logg.LevelInfo {
+ w = h.errWriter
+ } else {
+ w = h.outWriter
+ }
+
+ var prefix string
+ for _, field := range e.Fields {
+ if field.Name == FieldNameCmd {
+ prefix = fmt.Sprint(field.Value)
+ break
+ }
+ }
+
+ if prefix != "" {
+ prefix = prefix + ": "
+ }
+
+ if h.noLevelPrefix {
+ fmt.Fprintf(w, "%s%s", prefix, e.Message)
+ } else {
+ fmt.Fprintf(w, "%s %s%s", levelString[e.Level], prefix, e.Message)
+ }
+
+ for _, field := range e.Fields {
+ if strings.HasPrefix(field.Name, reservedFieldNamePrefix) {
+ continue
+ }
+ fmt.Fprintf(w, " %s %q", field.Name, field.Value)
+
+ }
+ fmt.Fprintln(w)
+
+ return nil
+}
diff --git a/common/loggers/ignorableLogger.go b/common/loggers/ignorableLogger.go
deleted file mode 100644
index c8aba560e..000000000
--- a/common/loggers/ignorableLogger.go
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright 2020 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package loggers
-
-import (
- "fmt"
-)
-
-// IgnorableLogger is a logger that ignores certain log statements.
-type IgnorableLogger interface {
- Logger
- Errorsf(statementID, format string, v ...any)
- Apply(logger Logger) IgnorableLogger
-}
-
-type ignorableLogger struct {
- Logger
- statements map[string]bool
-}
-
-// NewIgnorableLogger wraps the given logger and ignores the log statement IDs given.
-func NewIgnorableLogger(logger Logger, statements map[string]bool) IgnorableLogger {
- if statements == nil {
- statements = make(map[string]bool)
- }
- return ignorableLogger{
- Logger: logger,
- statements: statements,
- }
-}
-
-// Errorsf logs statementID as an ERROR if not configured as ignoreable.
-func (l ignorableLogger) Errorsf(statementID, format string, v ...any) {
- if l.statements[statementID] {
- // Ignore.
- return
- }
- ignoreMsg := fmt.Sprintf(`
-If you feel that this should not be logged as an ERROR, you can ignore it by adding this to your site config:
-ignoreErrors = [%q]`, statementID)
-
- format += ignoreMsg
-
- l.Errorf(format, v...)
-}
-
-func (l ignorableLogger) Apply(logger Logger) IgnorableLogger {
- return ignorableLogger{
- Logger: logger,
- statements: l.statements,
- }
-}
diff --git a/common/loggers/logger.go b/common/loggers/logger.go
new file mode 100644
index 000000000..85c75ef98
--- /dev/null
+++ b/common/loggers/logger.go
@@ -0,0 +1,303 @@
+// Copyright 2023 The Hugo Authors. All rights reserved.
+// Some functions in this file (see comments) is based on the Go source code,
+// copyright The Go Authors and governed by a BSD-style license.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package loggers
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/bep/logg"
+ "github.com/bep/logg/handlers/multi"
+ "github.com/gohugoio/hugo/common/terminal"
+)
+
+var (
+ reservedFieldNamePrefix = "__h_field_"
+ // FieldNameCmd is the name of the field that holds the command name.
+ FieldNameCmd = reservedFieldNamePrefix + "_cmd"
+ // Used to suppress statements.
+ FieldNameStatementID = reservedFieldNamePrefix + "__h_field_statement_id"
+)
+
+// Options defines options for the logger.
+type Options struct {
+ Level logg.Level
+ Stdout io.Writer
+ Stderr io.Writer
+ Distinct bool
+ StoreErrors bool
+ HandlerPost func(e *logg.Entry) error
+ SuppresssStatements map[string]bool
+}
+
+// New creates a new logger with the given options.
+func New(opts Options) Logger {
+ if opts.Stdout == nil {
+ opts.Stdout = os.Stdout
+ }
+ if opts.Stderr == nil {
+ opts.Stderr = os.Stdout
+ }
+ if opts.Level == 0 {
+ opts.Level = logg.LevelWarn
+ }
+
+ var logHandler logg.Handler
+ if terminal.PrintANSIColors(os.Stdout) {
+ logHandler = newDefaultHandler(opts.Stdout, opts.Stderr)
+ } else {
+ logHandler = newNoColoursHandler(opts.Stdout, opts.Stderr, false, nil)
+ }
+
+ errorsw := &strings.Builder{}
+ logCounters := newLogLevelCounter()
+ handlers := []logg.Handler{
+ whiteSpaceTrimmer(),
+ logHandler,
+ logCounters,
+ }
+
+ if opts.HandlerPost != nil {
+ var hookHandler logg.HandlerFunc = func(e *logg.Entry) error {
+ opts.HandlerPost(e)
+ return nil
+ }
+ handlers = append(handlers, hookHandler)
+ }
+
+ if opts.StoreErrors {
+ h := newNoColoursHandler(io.Discard, errorsw, true, func(e *logg.Entry) bool {
+ return e.Level >= logg.LevelError
+ })
+
+ handlers = append(handlers, h)
+ }
+
+ logHandler = multi.New(handlers...)
+
+ var logOnce *logOnceHandler
+ if opts.Distinct {
+ logOnce = newLogOnceHandler(logg.LevelWarn)
+ logHandler = newStopHandler(logOnce, logHandler)
+ }
+
+ if opts.SuppresssStatements != nil && len(opts.SuppresssStatements) > 0 {
+ logHandler = newStopHandler(newSuppressStatementsHandler(opts.SuppresssStatements), logHandler)
+ }
+
+ logger := logg.New(
+ logg.Options{
+ Level: opts.Level,
+ Handler: logHandler,
+ },
+ )
+
+ l := logger.WithLevel(opts.Level)
+
+ reset := func() {
+ logCounters.mu.Lock()
+ defer logCounters.mu.Unlock()
+ logCounters.counters = make(map[logg.Level]int)
+ errorsw.Reset()
+ if logOnce != nil {
+ logOnce.reset()
+ }
+ }
+
+ return &logAdapter{
+ logCounters: logCounters,
+ errors: errorsw,
+ reset: reset,
+ out: opts.Stdout,
+ level: opts.Level,
+ logger: logger,
+ debugl: l.WithLevel(logg.LevelDebug),
+ infol: l.WithLevel(logg.LevelInfo),
+ warnl: l.WithLevel(logg.LevelWarn),
+ errorl: l.WithLevel(logg.LevelError),
+ }
+}
+
+// NewDefault creates a new logger with the default options.
+func NewDefault() Logger {
+ opts := Options{
+ Distinct: true,
+ Level: logg.LevelWarn,
+ Stdout: os.Stdout,
+ Stderr: os.Stdout,
+ }
+ return New(opts)
+}
+
+func LevelLoggerToWriter(l logg.LevelLogger) io.Writer {
+ return logWriter{l: l}
+}
+
+type Logger interface {
+ Debugf(format string, v ...any)
+ Debugln(v ...any)
+ Error() logg.LevelLogger
+ Errorf(format string, v ...any)
+ Errorln(v ...any)
+ Errors() string
+ Errorsf(id, format string, v ...any)
+ Info() logg.LevelLogger
+ InfoCommand(command string) logg.LevelLogger
+ Infof(format string, v ...any)
+ Infoln(v ...any)
+ Level() logg.Level
+ LoggCount(logg.Level) int
+ Logger() logg.Logger
+ Out() io.Writer
+ Printf(format string, v ...any)
+ Println(v ...any)
+ PrintTimerIfDelayed(start time.Time, name string)
+ Reset()
+ Warn() logg.LevelLogger
+ WarnCommand(command string) logg.LevelLogger
+ Warnf(format string, v ...any)
+ Warnln(v ...any)
+}
+
+type logAdapter struct {
+ logCounters *logLevelCounter
+ errors *strings.Builder
+ reset func()
+ out io.Writer
+ level logg.Level
+ logger logg.Logger
+ debugl logg.LevelLogger
+ infol logg.LevelLogger
+ warnl logg.LevelLogger
+ errorl logg.LevelLogger
+}
+
+func (l *logAdapter) Debugf(format string, v ...any) {
+ l.debugl.Logf(format, v...)
+}
+
+func (l *logAdapter) Debugln(v ...any) {
+ l.debugl.Logf(l.sprint(v...))
+}
+
+func (l *logAdapter) Info() logg.LevelLogger {
+ return l.infol
+}
+
+func (l *logAdapter) InfoCommand(command string) logg.LevelLogger {
+ return l.infol.WithField(FieldNameCmd, command)
+}
+
+func (l *logAdapter) Infof(format string, v ...any) {
+ l.infol.Logf(format, v...)
+}
+
+func (l *logAdapter) Infoln(v ...any) {
+ l.infol.Logf(l.sprint(v...))
+}
+
+func (l *logAdapter) Level() logg.Level {
+ return l.level
+}
+
+func (l *logAdapter) LoggCount(level logg.Level) int {
+ l.logCounters.mu.RLock()
+ defer l.logCounters.mu.RUnlock()
+ return l.logCounters.counters[level]
+}
+
+func (l *logAdapter) Logger() logg.Logger {
+ return l.logger
+}
+
+func (l *logAdapter) Out() io.Writer {
+ return l.out
+}
+
+// PrintTimerIfDelayed prints a time statement to the FEEDBACK logger
+// if considerable time is spent.
+func (l *logAdapter) PrintTimerIfDelayed(start time.Time, name string) {
+ elapsed := time.Since(start)
+ milli := int(1000 * elapsed.Seconds())
+ if milli < 500 {
+ return
+ }
+ l.Printf("%s in %v ms", name, milli)
+}
+
+func (l *logAdapter) Printf(format string, v ...any) {
+ fmt.Fprintf(l.out, format, v...)
+}
+
+func (l *logAdapter) Println(v ...any) {
+ fmt.Fprintln(l.out, v...)
+}
+
+func (l *logAdapter) Reset() {
+ l.reset()
+}
+
+func (l *logAdapter) Warn() logg.LevelLogger {
+ return l.warnl
+}
+
+func (l *logAdapter) Warnf(format string, v ...any) {
+ l.warnl.Logf(format, v...)
+}
+
+func (l *logAdapter) WarnCommand(command string) logg.LevelLogger {
+ return l.warnl.WithField(FieldNameCmd, command)
+}
+
+func (l *logAdapter) Warnln(v ...any) {
+ l.warnl.Logf(l.sprint(v...))
+}
+
+func (l *logAdapter) Error() logg.LevelLogger {
+ return l.errorl
+}
+
+func (l *logAdapter) Errorf(format string, v ...any) {
+ l.errorl.Logf(format, v...)
+}
+
+func (l *logAdapter) Errorln(v ...any) {
+ l.errorl.Logf(l.sprint(v...))
+}
+
+func (l *logAdapter) Errors() string {
+ return l.errors.String()
+}
+
+func (l *logAdapter) Errorsf(id, format string, v ...any) {
+ l.errorl.WithField(FieldNameStatementID, id).Logf(format, v...)
+}
+
+func (l *logAdapter) sprint(v ...any) string {
+ return strings.TrimRight(fmt.Sprintln(v...), "\n")
+}
+
+type logWriter struct {
+ l logg.LevelLogger
+}
+
+func (w logWriter) Write(p []byte) (n int, err error) {
+ w.l.Logf("%s", p)
+ return len(p), nil
+}
diff --git a/common/loggers/logger_test.go b/common/loggers/logger_test.go
new file mode 100644
index 000000000..6aa540b0b
--- /dev/null
+++ b/common/loggers/logger_test.go
@@ -0,0 +1,156 @@
+// Copyright 2023 The Hugo Authors. All rights reserved.
+// Some functions in this file (see comments) is based on the Go source code,
+// copyright The Go Authors and governed by a BSD-style license.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package loggers_test
+
+import (
+ "io"
+ "strings"
+ "testing"
+
+ "github.com/bep/logg"
+ qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/common/loggers"
+)
+
+func TestLogDistinct(t *testing.T) {
+ c := qt.New(t)
+
+ opts := loggers.Options{
+ Distinct: true,
+ StoreErrors: true,
+ Stdout: io.Discard,
+ Stderr: io.Discard,
+ }
+
+ l := loggers.New(opts)
+
+ for i := 0; i < 10; i++ {
+ l.Errorln("error 1")
+ l.Errorln("error 2")
+ l.Warnln("warn 1")
+ }
+ c.Assert(strings.Count(l.Errors(), "error 1"), qt.Equals, 1)
+ c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 2)
+ c.Assert(l.LoggCount(logg.LevelWarn), qt.Equals, 1)
+}
+
+func TestHookLast(t *testing.T) {
+ c := qt.New(t)
+
+ opts := loggers.Options{
+ HandlerPost: func(e *logg.Entry) error {
+ panic(e.Message)
+ },
+ Stdout: io.Discard,
+ Stderr: io.Discard,
+ }
+
+ l := loggers.New(opts)
+
+ c.Assert(func() { l.Warnln("warn 1") }, qt.PanicMatches, "warn 1")
+}
+
+func TestOptionStoreErrors(t *testing.T) {
+ c := qt.New(t)
+
+ var sb strings.Builder
+
+ opts := loggers.Options{
+ StoreErrors: true,
+ Stderr: &sb,
+ Stdout: &sb,
+ }
+
+ l := loggers.New(opts)
+ l.Errorln("error 1")
+ l.Errorln("error 2")
+
+ errorsStr := l.Errors()
+
+ c.Assert(errorsStr, qt.Contains, "error 1")
+ c.Assert(errorsStr, qt.Not(qt.Contains), "ERROR")
+
+ c.Assert(sb.String(), qt.Contains, "error 1")
+ c.Assert(sb.String(), qt.Contains, "ERROR")
+
+}
+
+func TestLogCount(t *testing.T) {
+ c := qt.New(t)
+
+ opts := loggers.Options{
+ StoreErrors: true,
+ }
+
+ l := loggers.New(opts)
+ l.Errorln("error 1")
+ l.Errorln("error 2")
+ l.Warnln("warn 1")
+
+ c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 2)
+ c.Assert(l.LoggCount(logg.LevelWarn), qt.Equals, 1)
+ c.Assert(l.LoggCount(logg.LevelInfo), qt.Equals, 0)
+}
+
+func TestSuppressStatements(t *testing.T) {
+ c := qt.New(t)
+
+ opts := loggers.Options{
+ StoreErrors: true,
+ SuppresssStatements: map[string]bool{
+ "error-1": true,
+ },
+ }
+
+ l := loggers.New(opts)
+ l.Error().WithField(loggers.FieldNameStatementID, "error-1").Logf("error 1")
+ l.Errorln("error 2")
+
+ errorsStr := l.Errors()
+
+ c.Assert(errorsStr, qt.Not(qt.Contains), "error 1")
+ c.Assert(errorsStr, qt.Contains, "error 2")
+ c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 1)
+
+}
+
+func TestReset(t *testing.T) {
+ c := qt.New(t)
+
+ opts := loggers.Options{
+ StoreErrors: true,
+ Distinct: true,
+ Stdout: io.Discard,
+ Stderr: io.Discard,
+ }
+
+ l := loggers.New(opts)
+
+ for i := 0; i < 3; i++ {
+ l.Errorln("error 1")
+ l.Errorln("error 2")
+ l.Errorln("error 1")
+ c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 2)
+
+ l.Reset()
+
+ errorsStr := l.Errors()
+
+ c.Assert(errorsStr, qt.Equals, "")
+ c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 0)
+
+ }
+}
diff --git a/common/loggers/loggerglobal.go b/common/loggers/loggerglobal.go
new file mode 100644
index 000000000..92b2469ba
--- /dev/null
+++ b/common/loggers/loggerglobal.go
@@ -0,0 +1,53 @@
+// Copyright 2023 The Hugo Authors. All rights reserved.
+// Some functions in this file (see comments) is based on the Go source code,
+// copyright The Go Authors and governed by a BSD-style license.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package loggers
+
+import (
+ "sync"
+
+ "github.com/bep/logg"
+)
+
+func InitGlobalLogger(panicOnWarnings bool) {
+ logMu.Lock()
+ defer logMu.Unlock()
+ var logHookLast func(e *logg.Entry) error
+ if panicOnWarnings {
+ logHookLast = PanicOnWarningHook
+ }
+
+ log = New(
+ Options{
+ Distinct: true,
+ HandlerPost: logHookLast,
+ },
+ )
+}
+
+var logMu sync.Mutex
+
+func Log() Logger {
+ logMu.Lock()
+ defer logMu.Unlock()
+ return log
+}
+
+// The global logger.
+var log Logger
+</