diff --git a/commands/static_syncer.go b/commands/static_syncer.go index 23bdbe2d..5569d4de 100644 --- a/commands/static_syncer.go +++ b/commands/static_syncer.go @@ -58,7 +58,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error { syncer.DestFs = c.Fs.Destination // prevent spamming the log on changes - logger := helpers.NewDistinctFeedbackLogger() + logger := helpers.NewDistinctErrorLogger() for _, ev := range staticEvents { // Due to our approach of layering both directories and the content's rendered output diff --git a/common/loggers/ignorableLogger.go b/common/loggers/ignorableLogger.go index 766aae07..0a130900 100644 --- a/common/loggers/ignorableLogger.go +++ b/common/loggers/ignorableLogger.go @@ -22,6 +22,7 @@ import ( type IgnorableLogger interface { Logger Errorsf(statementID, format string, v ...interface{}) + Apply(logger Logger) IgnorableLogger } type ignorableLogger struct { @@ -55,3 +56,10 @@ ignoreErrors = [%q]`, statementID) l.Errorf(format, v...) } + +func (l ignorableLogger) Apply(logger Logger) IgnorableLogger { + return ignorableLogger{ + Logger: logger, + statements: l.statements, + } +} diff --git a/common/loggers/loggers.go b/common/loggers/loggers.go index c9b5d21b..4ed18801 100644 --- a/common/loggers/loggers.go +++ b/common/loggers/loggers.go @@ -59,6 +59,8 @@ type Logger interface { Println(v ...interface{}) PrintTimerIfDelayed(start time.Time, name string) Debug() *log.Logger + Debugf(format string, v ...interface{}) + Debugln(v ...interface{}) Info() *log.Logger Infof(format string, v ...interface{}) Infoln(v ...interface{}) @@ -108,6 +110,14 @@ func (l *logger) Debug() *log.Logger { return l.DEBUG } +func (l *logger) Debugf(format string, v ...interface{}) { + l.DEBUG.Printf(format, v...) +} + +func (l *logger) Debugln(v ...interface{}) { + l.DEBUG.Println(v...) +} + func (l *logger) Infof(format string, v ...interface{}) { l.INFO.Printf(format, v...) } diff --git a/deps/deps.go b/deps/deps.go index 36620c96..c0546db7 100644 --- a/deps/deps.go +++ b/deps/deps.go @@ -34,10 +34,7 @@ type Deps struct { Log loggers.Logger `json:"-"` // Used to log errors that may repeat itself many times. - DistinctErrorLog *helpers.DistinctLogger - - // Used to log warnings that may repeat itself many times. - DistinctWarningLog *helpers.DistinctLogger + LogDistinct loggers.Logger // The templates to use. This will usually implement the full tpl.TemplateManager. tmpl tpl.TemplateHandler @@ -266,14 +263,12 @@ func New(cfg DepsCfg) (*Deps, error) { ignoreErrors := cast.ToStringSlice(cfg.Cfg.Get("ignoreErrors")) ignorableLogger := loggers.NewIgnorableLogger(logger, ignoreErrors...) - distinctErrorLogger := helpers.NewDistinctLogger(logger.Error()) - distinctWarnLogger := helpers.NewDistinctLogger(logger.Warn()) + logDistinct := helpers.NewDistinctLogger(logger) d := &Deps{ Fs: fs, Log: ignorableLogger, - DistinctErrorLog: distinctErrorLogger, - DistinctWarningLog: distinctWarnLogger, + LogDistinct: logDistinct, templateProvider: cfg.TemplateProvider, translationProvider: cfg.TranslationProvider, WithTemplate: cfg.WithTemplate, diff --git a/docs/content/en/functions/errorf.md b/docs/content/en/functions/errorf.md index 450e9267..a20ad4f4 100644 --- a/docs/content/en/functions/errorf.md +++ b/docs/content/en/functions/errorf.md @@ -31,3 +31,22 @@ Both functions return an empty string, so the messages are only printed to the c ``` Note that `errorf` and `warnf` support all the formatting verbs of the [fmt](https://golang.org/pkg/fmt/) package. + +## Suppress errors + +Some times it may make sense to let the user suppress an ERROR and make the build succeed. + +You can do this by using the `erroridf` function. This functions takes an error ID as the first arument. + + +`` +{{ erroridf "my-custom-error" "You should consider fixing this."}} +``` + +This will produce: + +``` +ERROR 2021/06/07 17:47:38 You should consider fixing this. +If you feel that this should not be logged as an ERROR, you can ignore it by adding this to your site config: +ignoreErrors = ["my-custom-error"] +``` diff --git a/helpers/general.go b/helpers/general.go index 9589514e..09bf311f 100644 --- a/helpers/general.go +++ b/helpers/general.go @@ -29,6 +29,8 @@ import ( "unicode" "unicode/utf8" + "github.com/gohugoio/hugo/common/loggers" + "github.com/mitchellh/hashstructure" "github.com/gohugoio/hugo/hugofs" @@ -40,7 +42,6 @@ import ( "github.com/jdkato/prose/transform" bp "github.com/gohugoio/hugo/bufferpool" - jww "github.com/spf13/jwalterweatherman" "github.com/spf13/pflag" ) @@ -253,9 +254,9 @@ type LogPrinter interface { // DistinctLogger ignores duplicate log statements. type DistinctLogger struct { + loggers.Logger sync.RWMutex - getLogger func() LogPrinter - m map[string]bool + m map[string]bool } func (l *DistinctLogger) Reset() { @@ -270,52 +271,105 @@ func (l *DistinctLogger) Reset() { func (l *DistinctLogger) Println(v ...interface{}) { // fmt.Sprint doesn't add space between string arguments logStatement := strings.TrimSpace(fmt.Sprintln(v...)) - l.print(logStatement) + l.printIfNotPrinted("println", logStatement, func() { + l.Logger.Println(logStatement) + }) } // Printf will log the string returned from fmt.Sprintf given the arguments, // but not if it has been logged before. -// Note: A newline is appended. func (l *DistinctLogger) Printf(format string, v ...interface{}) { logStatement := fmt.Sprintf(format, v...) - l.print(logStatement) + l.printIfNotPrinted("printf", logStatement, func() { + l.Logger.Printf(format, v...) + }) } -func (l *DistinctLogger) print(logStatement string) { +func (l *DistinctLogger) Debugf(format string, v ...interface{}) { + logStatement := fmt.Sprintf(format, v...) + l.printIfNotPrinted("debugf", logStatement, func() { + l.Logger.Debugf(format, v...) + }) +} + +func (l *DistinctLogger) Debugln(v ...interface{}) { + logStatement := fmt.Sprint(v...) + l.printIfNotPrinted("debugln", logStatement, func() { + l.Logger.Debugln(v...) + }) +} + +func (l *DistinctLogger) Infof(format string, v ...interface{}) { + logStatement := fmt.Sprintf(format, v...) + l.printIfNotPrinted("info", logStatement, func() { + l.Logger.Infof(format, v...) + }) +} + +func (l *DistinctLogger) Infoln(v ...interface{}) { + logStatement := fmt.Sprint(v...) + l.printIfNotPrinted("infoln", logStatement, func() { + l.Logger.Infoln(v...) + }) +} + +func (l *DistinctLogger) Warnf(format string, v ...interface{}) { + logStatement := fmt.Sprintf(format, v...) + l.printIfNotPrinted("warnf", logStatement, func() { + l.Logger.Warnf(format, v...) + }) +} +func (l *DistinctLogger) Warnln(v ...interface{}) { + logStatement := fmt.Sprint(v...) + l.printIfNotPrinted("warnln", logStatement, func() { + l.Logger.Warnln(v...) + }) +} +func (l *DistinctLogger) Errorf(format string, v ...interface{}) { + logStatement := fmt.Sprint(v...) + l.printIfNotPrinted("errorf", logStatement, func() { + l.Logger.Errorf(format, v...) + }) +} + +func (l *DistinctLogger) Errorln(v ...interface{}) { + logStatement := fmt.Sprint(v...) + l.printIfNotPrinted("errorln", logStatement, func() { + l.Logger.Errorln(v...) + }) +} + +func (l *DistinctLogger) hasPrinted(key string) bool { l.RLock() - if l.m[logStatement] { - l.RUnlock() + defer l.RUnlock() + _, found := l.m[key] + return found +} + +func (l *DistinctLogger) printIfNotPrinted(level, logStatement string, print func()) { + key := level + logStatement + if l.hasPrinted(key) { return } - l.RUnlock() - l.Lock() - if !l.m[logStatement] { - l.getLogger().Println(logStatement) - l.m[logStatement] = true - } + print() + l.m[key] = true l.Unlock() } // NewDistinctErrorLogger creates a new DistinctLogger that logs ERRORs -func NewDistinctErrorLogger() *DistinctLogger { - return &DistinctLogger{m: make(map[string]bool), getLogger: func() LogPrinter { return jww.ERROR }} +func NewDistinctErrorLogger() loggers.Logger { + return &DistinctLogger{m: make(map[string]bool), Logger: loggers.NewErrorLogger()} } // NewDistinctLogger creates a new DistinctLogger that logs to the provided logger. -func NewDistinctLogger(logger LogPrinter) *DistinctLogger { - return &DistinctLogger{m: make(map[string]bool), getLogger: func() LogPrinter { return logger }} +func NewDistinctLogger(logger loggers.Logger) loggers.Logger { + return &DistinctLogger{m: make(map[string]bool), Logger: logger} } // NewDistinctWarnLogger creates a new DistinctLogger that logs WARNs -func NewDistinctWarnLogger() *DistinctLogger { - return &DistinctLogger{m: make(map[string]bool), getLogger: func() LogPrinter { return jww.WARN }} -} - -// NewDistinctFeedbackLogger creates a new DistinctLogger that can be used -// to give feedback to the user while not spamming with duplicates. -func NewDistinctFeedbackLogger() *DistinctLogger { - return &DistinctLogger{m: make(map[string]bool), getLogger: func() LogPrinter { return jww.FEEDBACK }} +func NewDistinctWarnLogger() loggers.Logger { + return &DistinctLogger{m: make(map[string]bool), Logger: loggers.NewWarningLogger()} } var ( @@ -324,16 +378,13 @@ var ( // DistinctWarnLog can be used to avoid spamming the logs with warnings. DistinctWarnLog = NewDistinctWarnLogger() - - // DistinctFeedbackLog can be used to avoid spamming the logs with info messages. - DistinctFeedbackLog = NewDistinctFeedbackLogger() ) // InitLoggers resets the global distinct loggers. func InitLoggers() { DistinctErrorLog.Reset() DistinctWarnLog.Reset() - DistinctFeedbackLog.Reset() + } // Deprecated informs about a deprecation, but only once for a given set of arguments' values. diff --git a/hugolib/alias.go b/hugolib/alias.go index 891098c9..2609cd6b 100644 --- a/hugolib/alias.go +++ b/hugolib/alias.go @@ -79,7 +79,7 @@ func (s *Site) writeDestAlias(path, permalink string, outputFormat output.Format func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p page.Page) (err error) { handler := newAliasHandler(s.Tmpl(), s.Log, allowRoot) - s.Log.Debug().Println("creating alias:", path, "redirecting to", permalink) + s.Log.Debugln("creating alias:", path, "redirecting to", permalink) targetPath, err := handler.targetPathAlias(path) if err != nil { diff --git a/hugolib/collections.go b/hugolib/collections.go index a794a986..9b4f83cc 100644 --- a/hugolib/collections.go +++ b/hugolib/collections.go @@ -28,7 +28,6 @@ var ( // implementations have no value on their own. // Slice is not meant to be used externally. It's a bridge function -// for the template functions. See collections.Slice. func (p *pageState) Slice(items interface{}) (interface{}, error) { return page.ToPages(items) } diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index a016cab9..0607bde1 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -579,7 +579,8 @@ func (h *HugoSites) resetLogs() { h.Log.Reset() loggers.GlobalErrorCounter.Reset() for _, s := range h.Sites { - s.Deps.DistinctErrorLog = helpers.NewDistinctLogger(h.Log.Error()) + s.Deps.Log.Reset() + s.Deps.LogDistinct.Reset() } } diff --git a/hugolib/page__new.go b/hugolib/page__new.go index b3763147..8c96d501 100644 --- a/hugolib/page__new.go +++ b/hugolib/page__new.go @@ -102,7 +102,7 @@ func newPageFromMeta( meta map[string]interface{}, metaProvider *pageMeta) (*pageState, error) { if metaProvider.f == nil { - metaProvider.f = page.NewZeroFile(metaProvider.s.DistinctWarningLog) + metaProvider.f = page.NewZeroFile(metaProvider.s.LogDistinct) } ps, err := newPageBase(metaProvider) diff --git a/hugolib/site.go b/hugolib/site.go index 3c7c03bd..12714892 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -1007,7 +1007,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro changeIdentities := make(identity.Identities) - s.Log.Debug().Printf("Rebuild for events %q", events) + s.Log.Debugf("Rebuild for events %q", events) h := s.h @@ -1026,7 +1026,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro sourceFilesChanged = make(map[string]bool) // prevent spamming the log on changes - logger = helpers.NewDistinctFeedbackLogger() + logger = helpers.NewDistinctErrorLogger() ) var cachePartitions []string @@ -1385,7 +1385,7 @@ func (s *Site) getMenusFromConfig() navigation.Menus { s.Log.Errorln(err) } else { for _, entry := range m { - s.Log.Debug().Printf("found menu: %q, in site config\n", name) + s.Log.Debugf("found menu: %q, in site config\n", name) menuEntry := navigation.MenuEntry{Menu: name} ime, err := maps.ToStringMapE(entry) @@ -1646,7 +1646,7 @@ func (s *Site) lookupLayouts(layouts ...string) tpl.Template { } func (s *Site) renderAndWriteXML(statCounter *uint64, name string, targetPath string, d interface{}, templ tpl.Template) error { - s.Log.Debug().Printf("Render XML for %q to %q", name, targetPath) + s.Log.Debugf("Render XML for %q to %q", name, targetPath) renderBuffer := bp.GetBuffer() defer bp.PutBuffer(renderBuffer) @@ -1668,7 +1668,7 @@ func (s *Site) renderAndWriteXML(statCounter *uint64, name string, targetPath st } func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, templ tpl.Template) error { - s.Log.Debug().Printf("Render %s to %q", name, targetPath) + s.Log.Debugf("Render %s to %q", name, targetPath) renderBuffer := bp.GetBuffer() defer bp.PutBuffer(renderBuffer) diff --git a/hugolib/site_render.go b/hugolib/site_render.go index 84293cfc..77ece780 100644 --- a/hugolib/site_render.go +++ b/hugolib/site_render.go @@ -389,13 +389,13 @@ func (s *Site) renderMainLanguageRedirect() error { mainLang := s.h.multilingual.DefaultLang if s.Info.defaultContentLanguageInSubdir { mainLangURL := s.PathSpec.AbsURL(mainLang.Lang+"/", false) - s.Log.Debug().Printf("Write redirect to main language %s: %s", mainLang, mainLangURL) + s.Log.Debugf("Write redirect to main language %s: %s", mainLang, mainLangURL) if err := s.publishDestAlias(true, "/", mainLangURL, html, nil); err != nil { return err } } else { mainLangURL := s.PathSpec.AbsURL("", false) - s.Log.Debug().Printf("Write redirect to main language %s: %s", mainLang, mainLangURL) + s.Log.Debugf("Write redirect to main language %s: %s", mainLang, mainLangURL) if err := s.publishDestAlias(true, mainLang.Lang, mainLangURL, html, nil); err != nil { return err } diff --git a/langs/i18n/i18n.go b/langs/i18n/i18n.go index 83d581a7..e45a1682 100644 --- a/langs/i18n/i18n.go +++ b/langs/i18n/i18n.go @@ -30,7 +30,7 @@ import ( type translateFunc func(translationID string, templateData interface{}) string -var i18nWarningLogger = helpers.NewDistinctFeedbackLogger() +var i18nWarningLogger = helpers.NewDistinctErrorLogger() // Translator handles i18n translations. type Translator struct { diff --git a/resources/page/zero_file.autogen.go b/resources/page/zero_file.autogen.go index 23e36b76..e4718a70 100644 --- a/resources/page/zero_file.autogen.go +++ b/resources/page/zero_file.autogen.go @@ -16,17 +16,17 @@ package page import ( - "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/source" ) // ZeroFile represents a zero value of source.File with warnings if invoked. type zeroFile struct { - log *helpers.DistinctLogger + log loggers.Logger } -func NewZeroFile(log *helpers.DistinctLogger) source.File { +func NewZeroFile(log loggers.Logger) source.File { return zeroFile{log: log} } diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go index 669e386f..0a9774aa 100644 --- a/tpl/collections/collections.go +++ b/tpl/collections/collections.go @@ -380,7 +380,7 @@ func (ns *Namespace) IsSet(a interface{}, key interface{}) (bool, error) { return av.MapIndex(kv).IsValid(), nil } default: - helpers.DistinctFeedbackLog.Printf("WARNING: calling IsSet with unsupported type %q (%T) will always return false.\n", av.Kind(), a) + helpers.DistinctErrorLog.Printf("WARNING: calling IsSet with unsupported type %q (%T) will always return false.\n", av.Kind(), a) } return false, nil diff --git a/tpl/data/resources_test.go b/tpl/data/resources_test.go index 5ad0f097..18061c05 100644 --- a/tpl/data/resources_test.go +++ b/tpl/data/resources_test.go @@ -213,12 +213,12 @@ func newDeps(cfg config.Provider) *deps.Deps { } return &deps.Deps{ - Cfg: cfg, - Fs: fs, - FileCaches: fileCaches, - ContentSpec: cs, - Log: logger, - DistinctErrorLog: helpers.NewDistinctLogger(logger.Error()), + Cfg: cfg, + Fs: fs, + FileCaches: fileCaches, + ContentSpec: cs, + Log: logger, + LogDistinct: helpers.NewDistinctLogger(logger), } } diff --git a/tpl/fmt/fmt.go b/tpl/fmt/fmt.go index 713088b5..9c16ca65 100644 --- a/tpl/fmt/fmt.go +++ b/tpl/fmt/fmt.go @@ -17,20 +17,22 @@ package fmt import ( _fmt "fmt" + "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" ) // New returns a new instance of the fmt-namespaced template functions. func New(d *deps.Deps) *Namespace { + ignorableLogger := d.Log.(loggers.IgnorableLogger) + distinctLogger := helpers.NewDistinctLogger(d.Log) ns := &Namespace{ - errorLogger: helpers.NewDistinctLogger(d.Log.Error()), - warnLogger: helpers.NewDistinctLogger(d.Log.Warn()), + distinctLogger: ignorableLogger.Apply(distinctLogger), } d.BuildStartListeners.Add(func() { - ns.errorLogger.Reset() - ns.warnLogger.Reset() + ns.distinctLogger.Reset() }) return ns @@ -38,8 +40,7 @@ func New(d *deps.Deps) *Namespace { // Namespace provides template functions for the "fmt" namespace. type Namespace struct { - errorLogger *helpers.DistinctLogger - warnLogger *helpers.DistinctLogger + distinctLogger loggers.IgnorableLogger } // Print returns string representation of the passed arguments. @@ -60,13 +61,21 @@ func (ns *Namespace) Println(a ...interface{}) string { // Errorf formats according to a format specifier and logs an ERROR. // It returns an empty string. func (ns *Namespace) Errorf(format string, a ...interface{}) string { - ns.errorLogger.Printf(format, a...) + ns.distinctLogger.Errorf(format, a...) + return "" +} + +// Erroridf formats according to a format specifier and logs an ERROR and +// an information text that the error with the given ID can be suppressed in config. +// It returns an empty string. +func (ns *Namespace) Erroridf(id, format string, a ...interface{}) string { + ns.distinctLogger.Errorsf(id, format, a...) return "" } // Warnf formats according to a format specifier and logs a WARNING. // It returns an empty string. func (ns *Namespace) Warnf(format string, a ...interface{}) string { - ns.warnLogger.Printf(format, a...) + ns.distinctLogger.Warnf(format, a...) return "" } diff --git a/tpl/fmt/init.go b/tpl/fmt/init.go index 6a2c9a85..f322f511 100644 --- a/tpl/fmt/init.go +++ b/tpl/fmt/init.go @@ -57,6 +57,13 @@ func init() { }, ) + ns.AddMethodMapping(ctx.Erroridf, + []string{"erroridf"}, + [][2]string{ + {`{{ erroridf "my-err-id" "%s." "failed" }}`, ``}, + }, + ) + ns.AddMethodMapping(ctx.Warnf, []string{"warnf"}, [][2]string{ diff --git a/tpl/fmt/init_test.go b/tpl/fmt/init_test.go index edc1dbb5..8fa3945b 100644 --- a/tpl/fmt/init_test.go +++ b/tpl/fmt/init_test.go @@ -30,7 +30,7 @@ func TestInit(t *testing.T) { var ns *internal.TemplateFuncsNamespace for _, nsf := range internal.TemplateFuncsNamespaceRegistry { - ns = nsf(&deps.Deps{Log: loggers.NewErrorLogger()}) + ns = nsf(&deps.Deps{Log: loggers.NewIgnorableLogger(loggers.NewErrorLogger())}) if ns.Name == name { found = true break