diff --git a/core/zerr/implementation.go b/core/zerr/implementation.go index 9a75d95..adad7fb 100644 --- a/core/zerr/implementation.go +++ b/core/zerr/implementation.go @@ -193,12 +193,12 @@ func (mu *Err) GetID() string { } func (mu *Err) Log(ctx context.Context) Error { - mu.logger.Log(ctx, mu) + mu.logger.LogError(ctx, mu) return mu } func (mu *Err) Notify(ctx context.Context, opts ...zoptions.NotifyOption) Error { - mu.notifier.Notify(ctx, mu, opts...) + mu.notifier.NotifyError(ctx, mu, opts...) return mu } diff --git a/core/zerr/wrap.go b/core/zerr/wrap.go index d9306f2..168b547 100644 --- a/core/zerr/wrap.go +++ b/core/zerr/wrap.go @@ -23,11 +23,11 @@ type Wrapper interface { } type Logger interface { - Log(ctx context.Context, err Error) + LogError(ctx context.Context, err Error) } type Notifier interface { - Notify(ctx context.Context, err Error, opts ...zoptions.NotifyOption) + NotifyError(ctx context.Context, err Error, opts ...zoptions.NotifyOption) } type WrapperFunc func(input WrapInitialInput) Error diff --git a/core/zlog/buffer.go b/core/zlog/buffer.go new file mode 100644 index 0000000..546770b --- /dev/null +++ b/core/zlog/buffer.go @@ -0,0 +1,75 @@ +package zlog + +import ( + "bytes" + "log/slog" + "sync" +) + +type jsonHandlerPool struct { + *sync.Pool +} + +func newJsonHandlerPool(handlerOption *slog.HandlerOptions) *jsonHandlerPool { + var handlerPool *jsonHandlerPool + generator := &sync.Pool{ + New: func() any { + buf := new(bytes.Buffer) + return &jsonHandler{ + Buffer: buf, + pool: handlerPool, + JSONHandler: slog.NewJSONHandler(buf, handlerOption), + } + }, + } + handlerPool = &jsonHandlerPool{ + generator, + } + return handlerPool +} + +func (jhp *jsonHandlerPool) Get() *jsonHandler { + handler := jhp.Pool.Get().(*jsonHandler) + return handler +} + +type jsonHandler struct { + *bytes.Buffer + pool *jsonHandlerPool + *slog.JSONHandler +} + +func (j *jsonHandler) Put() { + // Only put back to pool if buffer is smaller than 4MB in RAM size. + if j.Buffer.Cap() < 1024*1024*4 { + j.Buffer.Reset() + j.pool.Put(j) + } +} + +type bufferPool struct { + *sync.Pool +} + +func newBufferPool() *bufferPool { + return &bufferPool{ + &sync.Pool{ + New: func() any { + buf := new(bytes.Buffer) + buf.Grow(1024) + return buf + }, + }, + } +} + +func (bp *bufferPool) Get() *bytes.Buffer { + return bp.Pool.Get().(*bytes.Buffer) +} + +func (bp *bufferPool) Put(buf *bytes.Buffer) { + if buf.Cap() < 1024*1024*4 { + buf.Reset() + bp.Pool.Put(buf) + } +} diff --git a/core/zlog/caller.go b/core/zlog/caller.go new file mode 100644 index 0000000..d3e6811 --- /dev/null +++ b/core/zlog/caller.go @@ -0,0 +1 @@ +package zlog diff --git a/core/zlog/null.go b/core/zlog/null.go new file mode 100644 index 0000000..d3e6811 --- /dev/null +++ b/core/zlog/null.go @@ -0,0 +1 @@ +package zlog diff --git a/core/zlog/zlog.go b/core/zlog/zlog.go index ade69da..aaabadb 100644 --- a/core/zlog/zlog.go +++ b/core/zlog/zlog.go @@ -1,7 +1,141 @@ package zlog -import notifyv1 "gitlab.bareksa.com/backend/zen/internal/gen/proto/notify/v1" +import ( + "context" + "io" + "log/slog" + "sync" -func log() { - notifyv1.Payload + "github.com/fatih/color" +) + +type WriteLocker interface { + io.Writer + sync.Locker +} + +func WrapLocker(w io.Writer) WriteLocker { + if wl, ok := w.(WriteLocker); ok { + return wl + } + return &writeLocker{Writer: w} +} + +type writeLocker struct { + io.Writer + sync.Mutex +} + +type Log struct { + level slog.Level + bufPool *bufferPool + jsonPool *jsonHandlerPool + withAttrs []slog.Attr + withGroup []string + handlerOption *slog.HandlerOptions + pretty bool + color bool + writer WriteLocker +} + +// Clone clones the Logger, replacing the output writer with w. +// +// If w is nil, the clone shares the original Logger's output writer. +func (log *Log) Clone(w io.Writer) *Log { + var output WriteLocker + if w == nil { + output = log.writer + } else { + output = WrapLocker(w) + } + withAttrs := make([]slog.Attr, len(log.withAttrs)) + copy(withAttrs, log.withAttrs) + withGroup := make([]string, len(log.withGroup)) + copy(withGroup, log.withGroup) + return &Log{ + level: log.level, + withAttrs: withAttrs, + withGroup: withGroup, + handlerOption: log.handlerOption, + pretty: log.pretty, + bufPool: newBufferPool(), + jsonPool: newJsonHandlerPool(log.handlerOption), + writer: output, + } +} + +// Enabled implements the slog.Handler interface. +func (lo *Log) Enabled(ctx context.Context, lvl slog.Level) bool { + return lvl >= lo.level +} + +// Handle implements the slog.Handler interface. +func (lo *Log) Handle(ctx context.Context, record slog.Record) error { + if !lo.pretty { + j := lo.jsonPool.Get() + defer j.Put() + return j.Handle(ctx, record) + } + + var levelColor *color.Color + switch { + case record.Level >= slog.LevelError: + levelColor = color.New(color.FgRed) + case record.Level >= slog.LevelWarn: + levelColor = color.New(color.FgYellow) + case record.Level >= slog.LevelInfo: + levelColor = color.New(color.FgGreen) + default: + levelColor = color.New(color.FgWhite) + } + + _ = levelColor + + buf := lo.bufPool.Get() + defer lo.bufPool.Put(buf) + + panic("not implemented") + + // if record.PC != 0 && pr.opts.AddSource { + // frame := caller.From(record.PC).Frame + // levelColor.Fprint(buf, frame.File) + // levelColor.Fprint(buf, ":") + // levelColor.Fprint(buf, frame.Line) + // levelColor.Fprint(buf, " -- ") + // split := strings.Split(frame.Function, string(os.PathSeparator)) + // fnName := split[len(split)-1] + // levelColor.Fprint(buf, fnName) + // buf.WriteByte('\n') + // } +} + +// WithAttrs implements the slog.Handler interface. +func (lo *Log) WithAttrs(attrs []slog.Attr) slog.Handler { + l := lo.Clone(nil) + l.withAttrs = append(l.withAttrs, attrs...) + return l +} + +// WithGroup implements the slog.Handler interface. +func (lo *Log) WithGroup(name string) slog.Handler { + l := lo.Clone(nil) + l.withGroup = append(l.withGroup, name) + return l +} + +type replaceAttrFunc = func([]string, slog.Attr) slog.Attr + +// wrapReplaceAttr disables adding Time, Level, Source, Message +// fields when pretty mode is enabled. +func (lo *Log) wrapReplaceAttr(f replaceAttrFunc) replaceAttrFunc { + return func(s []string, a slog.Attr) slog.Attr { + if !lo.pretty { + return f(s, a) + } + switch a.Key { + case slog.TimeKey, slog.LevelKey, slog.SourceKey, slog.MessageKey: + return slog.Attr{} + } + return f(s, a) + } } diff --git a/core/zoptions/notify.go b/core/zoptions/notify.go index d9601bd..e8d3f14 100644 --- a/core/zoptions/notify.go +++ b/core/zoptions/notify.go @@ -1,6 +1,7 @@ package zoptions import ( + "fmt" "io" "os" "time" @@ -237,7 +238,12 @@ func NewNamedReaderFromFile(path string, mimetype string) NamedReader { if err != nil { rc = &errorReader{err} } else { - rc = f + if stat, err := f.Stat(); err == nil && stat.IsDir() { + f.Close() + rc = &errorReader{fmt.Errorf("file %s is a directory", path)} + } else { + rc = f + } } return &namedReader{ filename: path, diff --git a/go.mod b/go.mod index e1dd7b6..69708bd 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,14 @@ go 1.23 require ( connectrpc.com/connect v1.16.2 connectrpc.com/grpcreflect v1.2.0 + github.com/fatih/color v1.17.0 golang.org/x/net v0.23.0 google.golang.org/protobuf v1.34.2 ) -require golang.org/x/text v0.14.0 // indirect +require ( + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum index e0f11ee..0605658 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,21 @@ connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE= connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz5FRc= connectrpc.com/grpcreflect v1.2.0 h1:Q6og1S7HinmtbEuBvARLNwYmTbhEGRpHDhqrPNlmK+U= connectrpc.com/grpcreflect v1.2.0/go.mod h1:nwSOKmE8nU5u/CidgHtPYk1PFI3U9ignz7iDMxOYkSY= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +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= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=