diff --git a/.golangci.yml b/.golangci.yml index 6c27092..d3659fa 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -41,9 +41,8 @@ linters: # All available linters list: 0 { for j := 0; j < len(res); j++ { e.graph.AddEdge(graph.CollectsConnection, res[j].Plugin(), plugin) - /* - Here we need to init the - */ e.log.Debug("collects edge found", - slog.Any("method", res[j].Method()), - slog.Any("src", e.graph.VertexById(res[j].Plugin()).ID().String()), - slog.Any("dest", e.graph.VertexById(plugin).ID().String())) + zap.String("method", res[j].Method()), + zap.String("src", e.graph.VertexById(res[j].Plugin()).ID().String()), + zap.String("dest", e.graph.VertexById(plugin).ID().String())) } } } @@ -35,7 +32,7 @@ func (e *Endure) resolveCollectorEdges(plugin any) error { } // resolveEdges adds edges between the vertices -// At this point, we know all plugins, and all provides values +// At this point, we know all plugins and all 'provides' values func (e *Endure) resolveEdges() error { vertices := e.graph.Vertices() @@ -51,8 +48,8 @@ func (e *Endure) resolveEdges() error { if isPrimitive(initMethod.Type.In(j).String()) { e.log.Error( "primitive type in the function parameters", - slog.String("plugin", vertices[i].ID().String()), - slog.String("type", initMethod.Type.In(j).String()), + zap.String("plugin", vertices[i].ID().String()), + zap.String("type", initMethod.Type.In(j).String()), ) return errors.E("Init method should not receive primitive types (like string, int, etc). It should receive only interfaces") @@ -81,14 +78,14 @@ func (e *Endure) resolveEdges() error { // log e.log.Debug( "init edge found", - slog.Any("src", e.graph.VertexById(res[k].Plugin()).ID().String()), - slog.Any("dest", e.graph.VertexById(vertex.Plugin()).ID().String()), + zap.Any("src", e.graph.VertexById(res[k].Plugin()).ID().String()), + zap.Any("dest", e.graph.VertexById(vertex.Plugin()).ID().String()), ) } } } - // we should have here exact the same number of the deps implementing every particular arg + // we should have here exactly the same number of the deps implementing every particular arg if count != len(args[1:]) { // if there are no plugins that implement Init deps, remove this vertex from the tree del := e.graph.Remove(vertices[i].Plugin()) @@ -96,7 +93,7 @@ func (e *Endure) resolveEdges() error { e.registar.Remove(del[k].Plugin()) e.log.Debug( "plugin disabled, not enough Init dependencies", - slog.String("name", del[k].ID().String()), + zap.String("name", del[k].ID().String()), ) } @@ -117,8 +114,8 @@ func (e *Endure) resolveEdges() error { e.graph.TopologicalSort() - // notify user about the disabled plugins - // after topological sorting we remove all plugins with indegree > 0, because there are no edges to them + // to notify user about the disabled plugins + // after topological sorting, we remove all plugins with indegree > 0, because there are no edges to them if len(e.graph.TopologicalOrder()) != len(e.graph.Vertices()) { tpl := e.graph.TopologicalOrder() vrt := e.graph.Vertices() @@ -130,7 +127,7 @@ func (e *Endure) resolveEdges() error { for _, v := range vrt { if _, ok := tmpM[v.ID().String()]; !ok { - e.log.Warn("topological sort, plugin disabled", slog.String("plugin", v.ID().String())) + e.log.Warn("topological sort, plugin disabled", zap.String("plugin", v.ID().String())) } } } diff --git a/endure.go b/endure.go index 2edc48e..5d178f3 100644 --- a/endure.go +++ b/endure.go @@ -5,14 +5,15 @@ import ( "net/http" // pprof will be enabled in debug mode "net/http/pprof" - "os" "reflect" "sync" "time" "github.com/roadrunner-server/endure/v2/graph" + "github.com/roadrunner-server/endure/v2/logger" "github.com/roadrunner-server/endure/v2/registar" "github.com/roadrunner-server/errors" + "go.uber.org/zap" ) // Endure struct represent main endure repr @@ -24,7 +25,7 @@ type Endure struct { // Dependency graph graph *graph.Graph // log - log *slog.Logger + log *zap.Logger stopTimeout time.Duration profiler bool visualize bool @@ -34,7 +35,7 @@ type Endure struct { userResultsCh chan *Result } -// Options is the endure options +// Options is the 'endure' options type Options func(endure *Endure) // New returns empty endure container @@ -43,11 +44,15 @@ func New(level slog.Leveler, options ...Options) *Endure { level = slog.LevelDebug } + // error handling is omitted because we are sure that the logger will be created + zlog, _ := logger.BuildLogger(level) + c := &Endure{ registar: registar.New(), graph: graph.New(), mu: sync.RWMutex{}, stopTimeout: time.Second * 30, + log: zlog, } // Main thread channels @@ -59,14 +64,6 @@ func New(level slog.Leveler, options ...Options) *Endure { option(c) } - // create default logger if not already defined in the provided options - if c.log == nil { - opts := &slog.HandlerOptions{ - Level: level, - } - c.log = slog.New(slog.NewJSONHandler(os.Stderr, opts)) - } - // start profiler server if c.profiler { profile() @@ -97,7 +94,7 @@ func (e *Endure) Register(vertex any) error { */ if e.graph.HasVertex(vertex) { - e.log.Warn("already registered", slog.Any("error", errors.Errorf("plugin `%s` is already registered", t.String()))) + e.log.Warn("already registered", zap.Error(errors.Errorf("plugin `%s` is already registered", t.String()))) return nil } @@ -106,9 +103,9 @@ func (e *Endure) Register(vertex any) error { weight = val.Weight() e.log.Debug( "weight added", - slog.String("type", reflect.TypeOf(vertex).Elem().String()), - slog.String("kind", reflect.TypeOf(vertex).Elem().Kind().String()), - slog.Uint64("value", uint64(weight)), + zap.String("type", reflect.TypeOf(vertex).Elem().String()), + zap.String("kind", reflect.TypeOf(vertex).Elem().Kind().String()), + zap.Uint64("value", uint64(weight)), ) } @@ -119,9 +116,9 @@ func (e *Endure) Register(vertex any) error { e.log.Debug( "type registered", - slog.String("type", reflect.TypeOf(vertex).Elem().String()), - slog.String("kind", reflect.TypeOf(vertex).Elem().Kind().String()), - slog.String("method", "plugin"), + zap.String("type", reflect.TypeOf(vertex).Elem().String()), + zap.String("kind", reflect.TypeOf(vertex).Elem().Kind().String()), + zap.String("method", "plugin"), ) /* @@ -142,9 +139,9 @@ func (e *Endure) Register(vertex any) error { e.registar.Insert(vertex, outDeps[i].Type, outDeps[i].Method, weight) e.log.Debug( "provided type registered", - slog.String("type", outDeps[i].Type.String()), - slog.String("kind", outDeps[i].Type.Kind().String()), - slog.String("method", outDeps[i].Method), + zap.String("type", outDeps[i].Type.String()), + zap.String("kind", outDeps[i].Type.Kind().String()), + zap.String("method", outDeps[i].Method), ) } } diff --git a/go.mod b/go.mod index bbe8730..c15b798 100644 --- a/go.mod +++ b/go.mod @@ -4,4 +4,15 @@ go 1.23 toolchain go1.23.0 -require github.com/roadrunner-server/errors v1.4.1 +require ( + github.com/fatih/color v1.17.0 + github.com/roadrunner-server/errors v1.4.1 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/sys v0.18.0 // indirect +) diff --git a/go.sum b/go.sum index a08b8a3..d22adc4 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/roadrunner-server/errors v1.4.1 h1:LKNeaCGiwd3t8IaL840ZNF3UA9yDQlpvHnKddnh0YRQ= github.com/roadrunner-server/errors v1.4.1/go.mod h1:qeffnIKG0e4j1dzGpa+OGY5VKSfMphizvqWIw8s2lAo= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work.sum b/go.work.sum index 16d6c74..fece3d1 100644 --- a/go.work.sum +++ b/go.work.sum @@ -2,6 +2,8 @@ github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= diff --git a/graph/vertex.go b/graph/vertex.go index 7d6685a..8701437 100644 --- a/graph/vertex.go +++ b/graph/vertex.go @@ -4,7 +4,7 @@ import ( "reflect" ) -// Vertex is main vertex representation for the graph +// Vertex is the main vertex representation for the graph // since we can have cyclic dependencies // when we traverse the VerticesMap, we should mark nodes as visited or not to detect cycle type Vertex struct { diff --git a/init.go b/init.go index c2cc407..c55995e 100644 --- a/init.go +++ b/init.go @@ -1,10 +1,10 @@ package endure import ( - "log/slog" "reflect" "github.com/roadrunner-server/errors" + "go.uber.org/zap" ) func (e *Endure) init() error { @@ -33,7 +33,7 @@ func (e *Endure) init() error { inVals = append(inVals, reflect.ValueOf(vertices[i].Plugin())) // has deps if > 1 if len(args) > 1 { - // exclude first arg (it's receiver) + // exclude first arg (its receiver) arg := args[1:] for j := 0; j < len(arg); j++ { plugin := e.registar.ImplementsExcept(arg[j], vertices[i].Plugin()) @@ -43,7 +43,7 @@ func (e *Endure) init() error { e.registar.Remove(del[k].Plugin()) e.log.Debug( "plugin disabled, not enough Init dependencies", - slog.String("name", del[k].ID().String()), + zap.String("name", del[k].ID().String()), ) } @@ -51,7 +51,7 @@ func (e *Endure) init() error { } // check if the provided plugin dep has a method - // existence of the method indicates, that the dep provided by this plugin should be obtained via the method call + // existence of the method indicates that the dep provided by this plugin should be obtained via the method call switch plugin[0].Method() == "" { // we don't have a method, that means, plugin itself implements the dep case true: @@ -92,7 +92,7 @@ func (e *Endure) init() error { if errors.Is(errors.Disabled, ret[0].Interface().(error)) { e.log.Debug( "plugin disabled", - slog.String("name", vertices[i].ID().String()), + zap.String("name", vertices[i].ID().String()), ) // delete vertex and continue plugins := e.graph.Remove(vertices[i].Plugin()) @@ -100,7 +100,7 @@ func (e *Endure) init() error { for j := 0; j < len(plugins); j++ { e.log.Debug( "destination plugin disabled because root was disabled", - slog.String("name", plugins[j].ID().String()), + zap.String("name", plugins[j].ID().String()), ) e.registar.Remove(plugins[j].Plugin()) } diff --git a/logger/zap.go b/logger/zap.go new file mode 100644 index 0000000..d459699 --- /dev/null +++ b/logger/zap.go @@ -0,0 +1,159 @@ +package logger + +import ( + "log/slog" + "strings" + "time" + + "github.com/fatih/color" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// Mode represents available logger modes +type Mode string + +const ( + production Mode = "production" + development Mode = "development" +) + +// BuildLogger converts config into Zap configuration. +func BuildLogger(slevel slog.Leveler) (*zap.Logger, error) { + var mode Mode + var level string + var encoding string + + switch slevel { + case slog.LevelDebug: + mode = development + level = "debug" + encoding = "console" + case slog.LevelInfo: + mode = production + level = "info" + encoding = "json" + case slog.LevelWarn: + mode = production + level = "warning" + encoding = "json" + case slog.LevelError: + mode = production + level = "error" + encoding = "json" + default: + mode = development + level = "debug" + encoding = "console" + } + + var zCfg zap.Config + switch mode { + case production: + zCfg = zap.Config{ + Level: zap.NewAtomicLevelAt(zap.InfoLevel), + Development: false, + Encoding: "json", + EncoderConfig: zapcore.EncoderConfig{ + TimeKey: "ts", + LevelKey: "level", + NameKey: "logger", + CallerKey: zapcore.OmitKey, + FunctionKey: zapcore.OmitKey, + MessageKey: "msg", + StacktraceKey: zapcore.OmitKey, + EncodeLevel: zapcore.LowercaseLevelEncoder, + EncodeTime: utcEpochTimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + }, + OutputPaths: []string{"stderr"}, + ErrorOutputPaths: []string{"stderr"}, + } + case development: + zCfg = zap.Config{ + Level: zap.NewAtomicLevelAt(zap.DebugLevel), + Development: true, + Encoding: "console", + EncoderConfig: zapcore.EncoderConfig{ + TimeKey: "ts", + LevelKey: "level", + NameKey: "logger", + CallerKey: zapcore.OmitKey, + FunctionKey: zapcore.OmitKey, + MessageKey: "msg", + StacktraceKey: zapcore.OmitKey, + EncodeLevel: ColoredLevelEncoder, + EncodeName: ColoredNameEncoder, + EncodeTime: utcISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + }, + OutputPaths: []string{"stderr"}, + ErrorOutputPaths: []string{"stderr"}, + } + default: + zCfg = zap.Config{ + Level: zap.NewAtomicLevelAt(zap.DebugLevel), + Encoding: "console", + EncoderConfig: zapcore.EncoderConfig{ + TimeKey: "T", + LevelKey: "L", + NameKey: "N", + CallerKey: zapcore.OmitKey, + FunctionKey: zapcore.OmitKey, + MessageKey: "M", + StacktraceKey: zapcore.OmitKey, + EncodeLevel: ColoredLevelEncoder, + EncodeName: ColoredNameEncoder, + EncodeTime: utcISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + }, + OutputPaths: []string{"stderr"}, + ErrorOutputPaths: []string{"stderr"}, + } + } + + zlevel := zap.NewAtomicLevel() + if err := zlevel.UnmarshalText([]byte(level)); err == nil { + zCfg.Level = zlevel + } + + zCfg.Encoding = encoding + + return zCfg.Build() +} + +// ColoredLevelEncoder colorizes log levels. +func ColoredLevelEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { + switch level { + case zapcore.DebugLevel: + enc.AppendString(color.HiWhiteString(level.CapitalString())) + case zapcore.InfoLevel: + enc.AppendString(color.HiCyanString(level.CapitalString())) + case zapcore.WarnLevel: + enc.AppendString(color.HiYellowString(level.CapitalString())) + case zapcore.ErrorLevel, zapcore.DPanicLevel: + enc.AppendString(color.HiRedString(level.CapitalString())) + case zapcore.PanicLevel, zapcore.FatalLevel, zapcore.InvalidLevel: + enc.AppendString(color.HiMagentaString(level.CapitalString())) + } +} + +// ColoredNameEncoder colorizes service names. +func ColoredNameEncoder(s string, enc zapcore.PrimitiveArrayEncoder) { + if len(s) < 12 { + s += strings.Repeat(" ", 12-len(s)) + } + + enc.AppendString(color.HiGreenString(s)) +} + +func utcISO8601TimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.UTC().Format("2006-01-02T15:04:05-0700")) +} + +func utcEpochTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendInt64(t.UTC().UnixNano()) +} diff --git a/options.go b/options.go index 161e2d0..d269d84 100644 --- a/options.go +++ b/options.go @@ -1,7 +1,6 @@ package endure import ( - "log/slog" "time" ) @@ -23,15 +22,3 @@ func EnableProfiler() Options { endure.profiler = true } } - -// LogHandler defines the logger handler to create the slog.Logger -// -// For example: -// -// container = endure.New(slog.LevelInfo, LogHandler(slog.NewTextHandler( -// os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))) -func LogHandler(handler slog.Handler) Options { - return func(endure *Endure) { - endure.log = slog.New(handler) - } -} diff --git a/poller.go b/poller.go index aab2f43..eb061a2 100644 --- a/poller.go +++ b/poller.go @@ -1,7 +1,7 @@ package endure import ( - "log/slog" + "go.uber.org/zap" ) // poll is used to poll the errors from the vertex @@ -12,7 +12,7 @@ func (e *Endure) poll(r *result) { continue } // log error message - e.log.Error("plugin returned an error from the 'Serve' method", slog.Any("error", err), slog.String("plugin", res.vertexID)) + e.log.Error("plugin returned an error from the 'Serve' method", zap.Error(err), zap.String("plugin", res.vertexID)) // set the error res.err = err // send handleErrorCh signal @@ -25,7 +25,7 @@ func (e *Endure) startMainThread() { // main thread used to handle errors from vertices go func() { for res := range e.handleErrorCh { - e.log.Debug("processing error in the main thread", slog.String("id", res.vertexID)) + e.log.Debug("processing error in the main thread", zap.String("id", res.vertexID)) e.userResultsCh <- &Result{ Error: res.err, VertexID: res.vertexID, diff --git a/serve.go b/serve.go index a4b071d..5d1e532 100644 --- a/serve.go +++ b/serve.go @@ -1,12 +1,12 @@ package endure import ( - "log/slog" "reflect" "sort" "github.com/roadrunner-server/endure/v2/graph" "github.com/roadrunner-server/errors" + "go.uber.org/zap" ) func (e *Endure) serve() error { @@ -39,7 +39,7 @@ func (e *Endure) serve() error { var inVals []reflect.Value inVals = append(inVals, reflect.ValueOf(serveVertices[i].Plugin())) - e.log.Debug("calling serve method", slog.String("plugin", serveVertices[i].ID().String())) + e.log.Debug("calling serve method", zap.String("plugin", serveVertices[i].ID().String())) ret := serveMethod.Func.Call(inVals)[0].Interface() if ret != nil { diff --git a/stop.go b/stop.go index b40693a..daaf345 100644 --- a/stop.go +++ b/stop.go @@ -3,11 +3,11 @@ package endure import ( "context" stderr "errors" - "log/slog" "reflect" "sync" "github.com/roadrunner-server/errors" + "go.uber.org/zap" ) func (e *Endure) stop() error { @@ -46,7 +46,7 @@ func (e *Endure) stop() error { e.log.Debug( "calling stop function", - slog.String("plugin", vertices[i].ID().String()), + zap.String("plugin", vertices[i].ID().String()), ) ctx, cancel := context.WithTimeout(context.Background(), e.stopTimeout) @@ -54,7 +54,7 @@ func (e *Endure) stop() error { ret := stopMethod.Func.Call(inVals)[0].Interface() if ret != nil { - e.log.Error("failed to stop the plugin", slog.Any("error", ret.(error))) + e.log.Error("failed to stop the plugin", zap.String("name", vertices[i].ID().String()), zap.Error(ret.(error))) mu.Lock() errs = append(errs, ret.(error)) mu.Unlock()