tpl/fmt: Add erroridf template func

Fixes #8613
This commit is contained in:
Bjørn Erik Pedersen 2021-06-07 16:36:48 +02:00
parent 282f1aa3db
commit f55d2f4376
No known key found for this signature in database
GPG key ID: 330E6E2BD4859D8F
19 changed files with 170 additions and 71 deletions

View file

@ -58,7 +58,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
syncer.DestFs = c.Fs.Destination syncer.DestFs = c.Fs.Destination
// prevent spamming the log on changes // prevent spamming the log on changes
logger := helpers.NewDistinctFeedbackLogger() logger := helpers.NewDistinctErrorLogger()
for _, ev := range staticEvents { for _, ev := range staticEvents {
// Due to our approach of layering both directories and the content's rendered output // Due to our approach of layering both directories and the content's rendered output

View file

@ -22,6 +22,7 @@ import (
type IgnorableLogger interface { type IgnorableLogger interface {
Logger Logger
Errorsf(statementID, format string, v ...interface{}) Errorsf(statementID, format string, v ...interface{})
Apply(logger Logger) IgnorableLogger
} }
type ignorableLogger struct { type ignorableLogger struct {
@ -55,3 +56,10 @@ ignoreErrors = [%q]`, statementID)
l.Errorf(format, v...) l.Errorf(format, v...)
} }
func (l ignorableLogger) Apply(logger Logger) IgnorableLogger {
return ignorableLogger{
Logger: logger,
statements: l.statements,
}
}

View file

@ -59,6 +59,8 @@ type Logger interface {
Println(v ...interface{}) Println(v ...interface{})
PrintTimerIfDelayed(start time.Time, name string) PrintTimerIfDelayed(start time.Time, name string)
Debug() *log.Logger Debug() *log.Logger
Debugf(format string, v ...interface{})
Debugln(v ...interface{})
Info() *log.Logger Info() *log.Logger
Infof(format string, v ...interface{}) Infof(format string, v ...interface{})
Infoln(v ...interface{}) Infoln(v ...interface{})
@ -108,6 +110,14 @@ func (l *logger) Debug() *log.Logger {
return l.DEBUG 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{}) { func (l *logger) Infof(format string, v ...interface{}) {
l.INFO.Printf(format, v...) l.INFO.Printf(format, v...)
} }

11
deps/deps.go vendored
View file

@ -34,10 +34,7 @@ type Deps struct {
Log loggers.Logger `json:"-"` Log loggers.Logger `json:"-"`
// Used to log errors that may repeat itself many times. // Used to log errors that may repeat itself many times.
DistinctErrorLog *helpers.DistinctLogger LogDistinct loggers.Logger
// Used to log warnings that may repeat itself many times.
DistinctWarningLog *helpers.DistinctLogger
// The templates to use. This will usually implement the full tpl.TemplateManager. // The templates to use. This will usually implement the full tpl.TemplateManager.
tmpl tpl.TemplateHandler tmpl tpl.TemplateHandler
@ -266,14 +263,12 @@ func New(cfg DepsCfg) (*Deps, error) {
ignoreErrors := cast.ToStringSlice(cfg.Cfg.Get("ignoreErrors")) ignoreErrors := cast.ToStringSlice(cfg.Cfg.Get("ignoreErrors"))
ignorableLogger := loggers.NewIgnorableLogger(logger, ignoreErrors...) ignorableLogger := loggers.NewIgnorableLogger(logger, ignoreErrors...)
distinctErrorLogger := helpers.NewDistinctLogger(logger.Error()) logDistinct := helpers.NewDistinctLogger(logger)
distinctWarnLogger := helpers.NewDistinctLogger(logger.Warn())
d := &Deps{ d := &Deps{
Fs: fs, Fs: fs,
Log: ignorableLogger, Log: ignorableLogger,
DistinctErrorLog: distinctErrorLogger, LogDistinct: logDistinct,
DistinctWarningLog: distinctWarnLogger,
templateProvider: cfg.TemplateProvider, templateProvider: cfg.TemplateProvider,
translationProvider: cfg.TranslationProvider, translationProvider: cfg.TranslationProvider,
WithTemplate: cfg.WithTemplate, WithTemplate: cfg.WithTemplate,

View file

@ -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. 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"]
```

View file

@ -29,6 +29,8 @@ import (
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
"github.com/gohugoio/hugo/common/loggers"
"github.com/mitchellh/hashstructure" "github.com/mitchellh/hashstructure"
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
@ -40,7 +42,6 @@ import (
"github.com/jdkato/prose/transform" "github.com/jdkato/prose/transform"
bp "github.com/gohugoio/hugo/bufferpool" bp "github.com/gohugoio/hugo/bufferpool"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
@ -253,8 +254,8 @@ type LogPrinter interface {
// DistinctLogger ignores duplicate log statements. // DistinctLogger ignores duplicate log statements.
type DistinctLogger struct { type DistinctLogger struct {
loggers.Logger
sync.RWMutex sync.RWMutex
getLogger func() LogPrinter
m map[string]bool m map[string]bool
} }
@ -270,52 +271,105 @@ func (l *DistinctLogger) Reset() {
func (l *DistinctLogger) Println(v ...interface{}) { func (l *DistinctLogger) Println(v ...interface{}) {
// fmt.Sprint doesn't add space between string arguments // fmt.Sprint doesn't add space between string arguments
logStatement := strings.TrimSpace(fmt.Sprintln(v...)) 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, // Printf will log the string returned from fmt.Sprintf given the arguments,
// but not if it has been logged before. // but not if it has been logged before.
// Note: A newline is appended.
func (l *DistinctLogger) Printf(format string, v ...interface{}) { func (l *DistinctLogger) Printf(format string, v ...interface{}) {
logStatement := fmt.Sprintf(format, v...) 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() l.RLock()
if l.m[logStatement] { defer l.RUnlock()
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 return
} }
l.RUnlock()
l.Lock() l.Lock()
if !l.m[logStatement] { print()
l.getLogger().Println(logStatement) l.m[key] = true
l.m[logStatement] = true
}
l.Unlock() l.Unlock()
} }
// NewDistinctErrorLogger creates a new DistinctLogger that logs ERRORs // NewDistinctErrorLogger creates a new DistinctLogger that logs ERRORs
func NewDistinctErrorLogger() *DistinctLogger { func NewDistinctErrorLogger() loggers.Logger {
return &DistinctLogger{m: make(map[string]bool), getLogger: func() LogPrinter { return jww.ERROR }} return &DistinctLogger{m: make(map[string]bool), Logger: loggers.NewErrorLogger()}
} }
// NewDistinctLogger creates a new DistinctLogger that logs to the provided logger. // NewDistinctLogger creates a new DistinctLogger that logs to the provided logger.
func NewDistinctLogger(logger LogPrinter) *DistinctLogger { func NewDistinctLogger(logger loggers.Logger) loggers.Logger {
return &DistinctLogger{m: make(map[string]bool), getLogger: func() LogPrinter { return logger }} return &DistinctLogger{m: make(map[string]bool), Logger: logger}
} }
// NewDistinctWarnLogger creates a new DistinctLogger that logs WARNs // NewDistinctWarnLogger creates a new DistinctLogger that logs WARNs
func NewDistinctWarnLogger() *DistinctLogger { func NewDistinctWarnLogger() loggers.Logger {
return &DistinctLogger{m: make(map[string]bool), getLogger: func() LogPrinter { return jww.WARN }} return &DistinctLogger{m: make(map[string]bool), Logger: loggers.NewWarningLogger()}
}
// 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 }}
} }
var ( var (
@ -324,16 +378,13 @@ var (
// DistinctWarnLog can be used to avoid spamming the logs with warnings. // DistinctWarnLog can be used to avoid spamming the logs with warnings.
DistinctWarnLog = NewDistinctWarnLogger() DistinctWarnLog = NewDistinctWarnLogger()
// DistinctFeedbackLog can be used to avoid spamming the logs with info messages.
DistinctFeedbackLog = NewDistinctFeedbackLogger()
) )
// InitLoggers resets the global distinct loggers. // InitLoggers resets the global distinct loggers.
func InitLoggers() { func InitLoggers() {
DistinctErrorLog.Reset() DistinctErrorLog.Reset()
DistinctWarnLog.Reset() DistinctWarnLog.Reset()
DistinctFeedbackLog.Reset()
} }
// Deprecated informs about a deprecation, but only once for a given set of arguments' values. // Deprecated informs about a deprecation, but only once for a given set of arguments' values.

View file

@ -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) { func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p page.Page) (err error) {
handler := newAliasHandler(s.Tmpl(), s.Log, allowRoot) 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) targetPath, err := handler.targetPathAlias(path)
if err != nil { if err != nil {

View file

@ -28,7 +28,6 @@ var (
// implementations have no value on their own. // implementations have no value on their own.
// Slice is not meant to be used externally. It's a bridge function // 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) { func (p *pageState) Slice(items interface{}) (interface{}, error) {
return page.ToPages(items) return page.ToPages(items)
} }

View file

@ -579,7 +579,8 @@ func (h *HugoSites) resetLogs() {
h.Log.Reset() h.Log.Reset()
loggers.GlobalErrorCounter.Reset() loggers.GlobalErrorCounter.Reset()
for _, s := range h.Sites { for _, s := range h.Sites {
s.Deps.DistinctErrorLog = helpers.NewDistinctLogger(h.Log.Error()) s.Deps.Log.Reset()
s.Deps.LogDistinct.Reset()
} }
} }

View file

@ -102,7 +102,7 @@ func newPageFromMeta(
meta map[string]interface{}, meta map[string]interface{},
metaProvider *pageMeta) (*pageState, error) { metaProvider *pageMeta) (*pageState, error) {
if metaProvider.f == nil { if metaProvider.f == nil {
metaProvider.f = page.NewZeroFile(metaProvider.s.DistinctWarningLog) metaProvider.f = page.NewZeroFile(metaProvider.s.LogDistinct)
} }
ps, err := newPageBase(metaProvider) ps, err := newPageBase(metaProvider)

View file

@ -1007,7 +1007,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
changeIdentities := make(identity.Identities) changeIdentities := make(identity.Identities)
s.Log.Debug().Printf("Rebuild for events %q", events) s.Log.Debugf("Rebuild for events %q", events)
h := s.h h := s.h
@ -1026,7 +1026,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
sourceFilesChanged = make(map[string]bool) sourceFilesChanged = make(map[string]bool)
// prevent spamming the log on changes // prevent spamming the log on changes
logger = helpers.NewDistinctFeedbackLogger() logger = helpers.NewDistinctErrorLogger()
) )
var cachePartitions []string var cachePartitions []string
@ -1385,7 +1385,7 @@ func (s *Site) getMenusFromConfig() navigation.Menus {
s.Log.Errorln(err) s.Log.Errorln(err)
} else { } else {
for _, entry := range m { 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} menuEntry := navigation.MenuEntry{Menu: name}
ime, err := maps.ToStringMapE(entry) 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 { 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() renderBuffer := bp.GetBuffer()
defer bp.PutBuffer(renderBuffer) 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 { 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() renderBuffer := bp.GetBuffer()
defer bp.PutBuffer(renderBuffer) defer bp.PutBuffer(renderBuffer)

View file

@ -389,13 +389,13 @@ func (s *Site) renderMainLanguageRedirect() error {
mainLang := s.h.multilingual.DefaultLang mainLang := s.h.multilingual.DefaultLang
if s.Info.defaultContentLanguageInSubdir { if s.Info.defaultContentLanguageInSubdir {
mainLangURL := s.PathSpec.AbsURL(mainLang.Lang+"/", false) 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 { if err := s.publishDestAlias(true, "/", mainLangURL, html, nil); err != nil {
return err return err
} }
} else { } else {
mainLangURL := s.PathSpec.AbsURL("", false) 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 { if err := s.publishDestAlias(true, mainLang.Lang, mainLangURL, html, nil); err != nil {
return err return err
} }

View file

@ -30,7 +30,7 @@ import (
type translateFunc func(translationID string, templateData interface{}) string type translateFunc func(translationID string, templateData interface{}) string
var i18nWarningLogger = helpers.NewDistinctFeedbackLogger() var i18nWarningLogger = helpers.NewDistinctErrorLogger()
// Translator handles i18n translations. // Translator handles i18n translations.
type Translator struct { type Translator struct {

View file

@ -16,17 +16,17 @@
package page package page
import ( import (
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/source" "github.com/gohugoio/hugo/source"
) )
// ZeroFile represents a zero value of source.File with warnings if invoked. // ZeroFile represents a zero value of source.File with warnings if invoked.
type zeroFile struct { 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} return zeroFile{log: log}
} }

View file

@ -380,7 +380,7 @@ func (ns *Namespace) IsSet(a interface{}, key interface{}) (bool, error) {
return av.MapIndex(kv).IsValid(), nil return av.MapIndex(kv).IsValid(), nil
} }
default: 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 return false, nil

View file

@ -218,7 +218,7 @@ func newDeps(cfg config.Provider) *deps.Deps {
FileCaches: fileCaches, FileCaches: fileCaches,
ContentSpec: cs, ContentSpec: cs,
Log: logger, Log: logger,
DistinctErrorLog: helpers.NewDistinctLogger(logger.Error()), LogDistinct: helpers.NewDistinctLogger(logger),
} }
} }

View file

@ -17,20 +17,22 @@ package fmt
import ( import (
_fmt "fmt" _fmt "fmt"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
) )
// New returns a new instance of the fmt-namespaced template functions. // New returns a new instance of the fmt-namespaced template functions.
func New(d *deps.Deps) *Namespace { func New(d *deps.Deps) *Namespace {
ignorableLogger := d.Log.(loggers.IgnorableLogger)
distinctLogger := helpers.NewDistinctLogger(d.Log)
ns := &Namespace{ ns := &Namespace{
errorLogger: helpers.NewDistinctLogger(d.Log.Error()), distinctLogger: ignorableLogger.Apply(distinctLogger),
warnLogger: helpers.NewDistinctLogger(d.Log.Warn()),
} }
d.BuildStartListeners.Add(func() { d.BuildStartListeners.Add(func() {
ns.errorLogger.Reset() ns.distinctLogger.Reset()
ns.warnLogger.Reset()
}) })
return ns return ns
@ -38,8 +40,7 @@ func New(d *deps.Deps) *Namespace {
// Namespace provides template functions for the "fmt" namespace. // Namespace provides template functions for the "fmt" namespace.
type Namespace struct { type Namespace struct {
errorLogger *helpers.DistinctLogger distinctLogger loggers.IgnorableLogger
warnLogger *helpers.DistinctLogger
} }
// Print returns string representation of the passed arguments. // 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. // Errorf formats according to a format specifier and logs an ERROR.
// It returns an empty string. // It returns an empty string.
func (ns *Namespace) Errorf(format string, a ...interface{}) 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 "" return ""
} }
// Warnf formats according to a format specifier and logs a WARNING. // Warnf formats according to a format specifier and logs a WARNING.
// It returns an empty string. // It returns an empty string.
func (ns *Namespace) Warnf(format string, a ...interface{}) string { func (ns *Namespace) Warnf(format string, a ...interface{}) string {
ns.warnLogger.Printf(format, a...) ns.distinctLogger.Warnf(format, a...)
return "" return ""
} }

View file

@ -57,6 +57,13 @@ func init() {
}, },
) )
ns.AddMethodMapping(ctx.Erroridf,
[]string{"erroridf"},
[][2]string{
{`{{ erroridf "my-err-id" "%s." "failed" }}`, ``},
},
)
ns.AddMethodMapping(ctx.Warnf, ns.AddMethodMapping(ctx.Warnf,
[]string{"warnf"}, []string{"warnf"},
[][2]string{ [][2]string{

View file

@ -30,7 +30,7 @@ func TestInit(t *testing.T) {
var ns *internal.TemplateFuncsNamespace var ns *internal.TemplateFuncsNamespace
for _, nsf := range internal.TemplateFuncsNamespaceRegistry { 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 { if ns.Name == name {
found = true found = true
break break