diff options
Diffstat (limited to 'common')
-rw-r--r-- | common/loggers/handlerdefault.go | 106 | ||||
-rw-r--r-- | common/loggers/handlersmisc.go | 158 | ||||
-rw-r--r-- | common/loggers/handlerterminal.go | 90 | ||||
-rw-r--r-- | common/loggers/ignorableLogger.go | 63 | ||||
-rw-r--r-- | common/loggers/logger.go | 303 | ||||
-rw-r--r-- | common/loggers/logger_test.go | 156 | ||||
-rw-r--r-- | common/loggers/loggerglobal.go | 53 | ||||
-rw-r--r-- | common/loggers/loggers.go | 355 | ||||
-rw-r--r-- | common/loggers/loggers_test.go | 60 |
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 |") -} |