diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2023-06-16 08:17:42 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2023-06-18 13:03:04 +0200 |
commit | 7c9fada778e91976d4ba1cbe942235a9bbeaf5cb (patch) | |
tree | a717f6e0a5915777ae6859564acd13385213bbab /common | |
parent | 0e7944658660b5658b7640dce3cb346d7198d8c9 (diff) |
Replace the old log setup, with structured logging etc.
Fixes #11124
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 |