aboutsummaryrefslogtreecommitdiffhomepage
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
+
+func init() {
+ InitGlobalLogger(false)
+}
diff --git a/common/loggers/loggers.go b/common/loggers/loggers.go
deleted file mode 100644
index fbbbca435..000000000
--- a/common/loggers/loggers.go
+++ /dev/null
@@ -1,355 +0,0 @@
-// Copyright 2020 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package loggers
-
-import (
- "bytes"
- "fmt"
- "io"
- "log"
- "os"
- "regexp"
- "runtime"
- "sync/atomic"
- "time"
-
- "github.com/gohugoio/hugo/common/terminal"
-
- jww "github.com/spf13/jwalterweatherman"
-)
-
-var (
- // Counts ERROR logs to the global jww logger.
- GlobalErrorCounter *jww.Counter
- PanicOnWarning atomic.Bool
-)
-
-func init() {
- GlobalErrorCounter = &jww.Counter{}
- jww.SetLogListeners(jww.LogCounter(GlobalErrorCounter, jww.LevelError))
-}
-
-func LoggerToWriterWithPrefix(logger *log.Logger, prefix string) io.Writer {
- return prefixWriter{
- logger: logger,
- prefix: prefix,
- }
-}
-
-type prefixWriter struct {
- logger *log.Logger
- prefix string
-}
-
-func (w prefixWriter) Write(p []byte) (n int, err error) {
- w.logger.Printf("%s: %s", w.prefix, p)
- return len(p), nil
-}
-
-type Logger interface {
- Printf(format string, v ...any)
- Println(v ...any)
- PrintTimerIfDelayed(start time.Time, name string)
- Debug() *log.Logger
- Debugf(format string, v ...any)
- Debugln(v ...any)
- Info() *log.Logger
- Infof(format string, v ...any)
- Infoln(v ...any)
- Warn() *log.Logger
- Warnf(format string, v ...any)
- Warnln(v ...any)
- Error() *log.Logger
- Errorf(format string, v ...any)
- Errorln(v ...any)
- Errors() string
-
- Out() io.Writer
-
- Reset()
-
- // Used in tests.
- LogCounters() *LogCounters
-}
-
-type LogCounters struct {
- ErrorCounter *jww.Counter
- WarnCounter *jww.Counter
-}
-
-type logger struct {
- *jww.Notepad
-
- // The writer that represents stdout.
- // Will be io.Discard when in quiet mode.
- out io.Writer
-
- logCounters *LogCounters
-
- // This is only set in server mode.
- errors *bytes.Buffer
-}
-
-func (l *logger) Printf(format string, v ...any) {
- l.FEEDBACK.Printf(format, v...)
-}
-
-func (l *logger) Println(v ...any) {
- l.FEEDBACK.Println(v...)
-}
-
-func (l *logger) Debug() *log.Logger {
- return l.DEBUG
-}
-
-func (l *logger) Debugf(format string, v ...any) {
- l.DEBUG.Printf(format, v...)
-}
-
-func (l *logger) Debugln(v ...any) {
- l.DEBUG.Println(v...)
-}
-
-func (l *logger) Infof(format string, v ...any) {
- l.INFO.Printf(format, v...)
-}
-
-func (l *logger) Infoln(v ...any) {
- l.INFO.Println(v...)
-}
-
-func (l *logger) Info() *log.Logger {
- return l.INFO
-}
-
-const panicOnWarningMessage = "Warning trapped. Remove the --panicOnWarning flag to continue."
-
-func (l *logger) Warnf(format string, v ...any) {
- l.WARN.Printf(format, v...)
- if PanicOnWarning.Load() {
- panic(panicOnWarningMessage)
- }
-}
-
-func (l *logger) Warnln(v ...any) {
- l.WARN.Println(v...)
- if PanicOnWarning.Load() {
- panic(panicOnWarningMessage)
- }
-}
-
-func (l *logger) Warn() *log.Logger {
- return l.WARN
-}
-
-func (l *logger) Errorf(format string, v ...any) {
- l.ERROR.Printf(format, v...)
-}
-
-func (l *logger) Errorln(v ...any) {
- l.ERROR.Println(v...)
-}
-
-func (l *logger) Error() *log.Logger {
- return l.ERROR
-}
-
-func (l *logger) LogCounters() *LogCounters {
- return l.logCounters
-}
-
-func (l *logger) Out() io.Writer {
- return l.out
-}
-
-// PrintTimerIfDelayed prints a time statement to the FEEDBACK logger
-// if considerable time is spent.
-func (l *logger) PrintTimerIfDelayed(start time.Time, name string) {
- elapsed := time.Since(start)
- milli := int(1000 * elapsed.Seconds())
- if milli < 500 {
- return
- }
- l.Printf("%s in %v ms", name, milli)
-}
-
-func (l *logger) PrintTimer(start time.Time, name string) {
- elapsed := time.Since(start)
- milli := int(1000 * elapsed.Seconds())
- l.Printf("%s in %v ms", name, milli)
-}
-
-func (l *logger) Errors() string {
- if l.errors == nil {
- return ""
- }
- return ansiColorRe.ReplaceAllString(l.errors.String(), "")
-}
-
-// Reset resets the logger's internal state.
-func (l *logger) Reset() {
- l.logCounters.ErrorCounter.Reset()
- if l.errors != nil {
- l.errors.Reset()
- }
-}
-
-// NewLogger creates a new Logger for the given thresholds
-func NewLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) Logger {
- return newLogger(stdoutThreshold, logThreshold, outHandle, logHandle, saveErrors)
-}
-
-// NewDebugLogger is a convenience function to create a debug logger.
-func NewDebugLogger() Logger {
- return NewBasicLogger(jww.LevelDebug)
-}
-
-// NewWarningLogger is a convenience function to create a warning logger.
-func NewWarningLogger() Logger {
- return NewBasicLogger(jww.LevelWarn)
-}
-
-// NewInfoLogger is a convenience function to create a info logger.
-func NewInfoLogger() Logger {
- return NewBasicLogger(jww.LevelInfo)
-}
-
-// NewErrorLogger is a convenience function to create an error logger.
-func NewErrorLogger() Logger {
- return NewBasicLogger(jww.LevelError)
-}
-
-// NewBasicLogger creates a new basic logger writing to Stdout.
-func NewBasicLogger(t jww.Threshold) Logger {
- return newLogger(t, jww.LevelError, os.Stdout, io.Discard, false)
-}
-
-// NewBasicLoggerForWriter creates a new basic logger writing to w.
-func NewBasicLoggerForWriter(t jww.Threshold, w io.Writer) Logger {
- return newLogger(t, jww.LevelError, w, io.Discard, false)
-}
-
-// RemoveANSIColours removes all ANSI colours from the given string.
-func RemoveANSIColours(s string) string {
- return ansiColorRe.ReplaceAllString(s, "")
-}
-
-var (
- ansiColorRe = regexp.MustCompile("(?s)\\033\\[\\d*(;\\d*)*m")
- errorRe = regexp.MustCompile("^(ERROR|FATAL|WARN)")
-)
-
-type ansiCleaner struct {
- w io.Writer
-}
-
-func (a ansiCleaner) Write(p []byte) (n int, err error) {
- return a.w.Write(ansiColorRe.ReplaceAll(p, []byte("")))
-}
-
-type labelColorizer struct {
- w io.Writer
-}
-
-func (a labelColorizer) Write(p []byte) (n int, err error) {
- replaced := errorRe.ReplaceAllStringFunc(string(p), func(m string) string {
- switch m {
- case "ERROR", "FATAL":
- return terminal.Error(m)
- case "WARN":
- return terminal.Warning(m)
- default:
- return m
- }
- })
- // io.MultiWriter will abort if we return a bigger write count than input
- // bytes, so we lie a little.
- _, err = a.w.Write([]byte(replaced))
- return len(p), err
-}
-
-// InitGlobalLogger initializes the global logger, used in some rare cases.
-func InitGlobalLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer) {
- outHandle, logHandle = getLogWriters(outHandle, logHandle)
-
- jww.SetStdoutOutput(outHandle)
- jww.SetLogOutput(logHandle)
- jww.SetLogThreshold(logThreshold)
- jww.SetStdoutThreshold(stdoutThreshold)
-}
-
-func getLogWriters(outHandle, logHandle io.Writer) (io.Writer, io.Writer) {
- isTerm := terminal.PrintANSIColors(os.Stdout)
- if logHandle != io.Discard && isTerm {
- // Remove any Ansi coloring from log output
- logHandle = ansiCleaner{w: logHandle}
- }
-
- if isTerm {
- outHandle = labelColorizer{w: outHandle}
- }
-
- return outHandle, logHandle
-}
-
-type fatalLogWriter int
-
-func (s fatalLogWriter) Write(p []byte) (n int, err error) {
- trace := make([]byte, 1500)
- runtime.Stack(trace, true)
- fmt.Printf("\n===========\n\n%s\n", trace)
- os.Exit(-1)
-
- return 0, nil
-}
-
-var fatalLogListener = func(t jww.Threshold) io.Writer {
- if t != jww.LevelError {
- // Only interested in ERROR
- return nil
- }
-
- return new(fatalLogWriter)
-}
-
-func newLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *logger {
- errorCounter := &jww.Counter{}
- warnCounter := &jww.Counter{}
- outHandle, logHandle = getLogWriters(outHandle, logHandle)
-
- listeners := []jww.LogListener{jww.LogCounter(errorCounter, jww.LevelError), jww.LogCounter(warnCounter, jww.LevelWarn)}
- var errorBuff *bytes.Buffer
- if saveErrors {
- errorBuff = new(bytes.Buffer)
- errorCapture := func(t jww.Threshold) io.Writer {
- if t != jww.LevelError {
- // Only interested in ERROR
- return nil
- }
- return errorBuff
- }
-
- listeners = append(listeners, errorCapture)
- }
-
- return &logger{
- Notepad: jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime, listeners...),
- out: outHandle,
- logCounters: &LogCounters{
- ErrorCounter: errorCounter,
- WarnCounter: warnCounter,
- },
- errors: errorBuff,
- }
-}
diff --git a/common/loggers/loggers_test.go b/common/loggers/loggers_test.go
deleted file mode 100644
index a7bd1ae12..000000000
--- a/common/loggers/loggers_test.go
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright 2018 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package loggers
-
-import (
- "bytes"
- "fmt"
- "log"
- "testing"
-
- qt "github.com/frankban/quicktest"
-)
-
-func TestLogger(t *testing.T) {
- c := qt.New(t)
- l := NewWarningLogger()
-
- l.Errorln("One error")
- l.Errorln("Two error")
- l.Warnln("A warning")
-
- c.Assert(l.LogCounters().ErrorCounter.Count(), qt.Equals, uint64(2))
-}
-
-func TestLoggerToWriterWithPrefix(t *testing.T) {
- c := qt.New(t)
-
- var b bytes.Buffer
-
- logger := log.New(&b, "", 0)
-
- w := LoggerToWriterWithPrefix(logger, "myprefix")
-
- fmt.Fprint(w, "Hello Hugo!")
-
- c.Assert(b.String(), qt.Equals, "myprefix: Hello Hugo!\n")
-}
-
-func TestRemoveANSIColours(t *testing.T) {
- c := qt.New(t)
-
- c.Assert(RemoveANSIColours(""), qt.Equals, "")
- c.Assert(RemoveANSIColours("\033[31m"), qt.Equals, "")
- c.Assert(RemoveANSIColours("\033[31mHello"), qt.Equals, "Hello")
- c.Assert(RemoveANSIColours("\033[31mHello\033[0m"), qt.Equals, "Hello")
- c.Assert(RemoveANSIColours("\033[31mHello\033[0m World"), qt.Equals, "Hello World")
- c.Assert(RemoveANSIColours("\033[31mHello\033[0m World\033[31m!"), qt.Equals, "Hello World!")
- c.Assert(RemoveANSIColours("\x1b[90m 5 |"), qt.Equals, " 5 |")
-}